/* |
|
* Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved. |
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
|
* |
|
* This code is free software; you can redistribute it and/or modify it |
|
* under the terms of the GNU General Public License version 2 only, as |
|
* published by the Free Software Foundation. Oracle designates this |
|
* particular file as subject to the "Classpath" exception as provided |
|
* by Oracle in the LICENSE file that accompanied this code. |
|
* |
|
* This code is distributed in the hope that it will be useful, but WITHOUT |
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
|
* version 2 for more details (a copy is included in the LICENSE file that |
|
* accompanied this code). |
|
* |
|
* You should have received a copy of the GNU General Public License version |
|
* 2 along with this work; if not, write to the Free Software Foundation, |
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
|
* |
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
|
* or visit www.oracle.com if you need additional information or have any |
|
* questions. |
|
*/ |
|
package javax.swing.text; |
|
import com.sun.beans.util.Cache; |
|
import java.security.AccessController; |
|
import java.security.PrivilegedAction; |
|
import java.beans.Transient; |
|
import java.util.HashMap; |
|
import java.util.Hashtable; |
|
import java.util.Enumeration; |
|
import java.util.Vector; |
|
import java.util.concurrent.*; |
|
import java.io.*; |
|
import java.awt.*; |
|
import java.awt.event.*; |
|
import java.awt.print.*; |
|
import java.awt.datatransfer.*; |
|
import java.awt.im.InputContext; |
|
import java.awt.im.InputMethodRequests; |
|
import java.awt.font.TextHitInfo; |
|
import java.awt.font.TextAttribute; |
|
import java.awt.print.Printable; |
|
import java.awt.print.PrinterException; |
|
import javax.print.PrintService; |
|
import javax.print.attribute.PrintRequestAttributeSet; |
|
import java.text.*; |
|
import java.text.AttributedCharacterIterator.Attribute; |
|
import javax.swing.*; |
|
import javax.swing.event.*; |
|
import javax.swing.plaf.*; |
|
import javax.accessibility.*; |
|
import javax.print.attribute.*; |
|
import sun.awt.AppContext; |
|
import sun.swing.PrintingStatus; |
|
import sun.swing.SwingUtilities2; |
|
import sun.swing.text.TextComponentPrintable; |
|
import sun.swing.SwingAccessor; |
|
/** |
|
* <code>JTextComponent</code> is the base class for swing text |
|
* components. It tries to be compatible with the |
|
* <code>java.awt.TextComponent</code> class |
|
* where it can reasonably do so. Also provided are other services |
|
* for additional flexibility (beyond the pluggable UI and bean |
|
* support). |
|
* You can find information on how to use the functionality |
|
* this class provides in |
|
* <a href="https://docs.oracle.com/javase/tutorial/uiswing/components/generaltext.html">General Rules for Using Text Components</a>, |
|
* a section in <em>The Java Tutorial.</em> |
|
* |
|
* <dl> |
|
* <dt><b><font size=+1>Caret Changes</font></b> |
|
* <dd> |
|
* The caret is a pluggable object in swing text components. |
|
* Notification of changes to the caret position and the selection |
|
* are sent to implementations of the <code>CaretListener</code> |
|
* interface that have been registered with the text component. |
|
* The UI will install a default caret unless a customized caret |
|
* has been set. <br> |
|
* By default the caret tracks all the document changes |
|
* performed on the Event Dispatching Thread and updates it's position |
|
* accordingly if an insertion occurs before or at the caret position |
|
* or a removal occurs before the caret position. <code>DefaultCaret</code> |
|
* tries to make itself visible which may lead to scrolling |
|
* of a text component within <code>JScrollPane</code>. The default caret |
|
* behavior can be changed by the {@link DefaultCaret#setUpdatePolicy} method. |
|
* <br> |
|
* <b>Note</b>: Non-editable text components also have a caret though |
|
* it may not be painted. |
|
* |
|
* <dt><b><font size=+1>Commands</font></b> |
|
* <dd> |
|
* Text components provide a number of commands that can be used |
|
* to manipulate the component. This is essentially the way that |
|
* the component expresses its capabilities. These are expressed |
|
* in terms of the swing <code>Action</code> interface, |
|
* using the <code>TextAction</code> implementation. |
|
* The set of commands supported by the text component can be |
|
* found with the {@link #getActions} method. These actions |
|
* can be bound to key events, fired from buttons, etc. |
|
* |
|
* <dt><b><font size=+1>Text Input</font></b> |
|
* <dd> |
|
* The text components support flexible and internationalized text input, using |
|
* keymaps and the input method framework, while maintaining compatibility with |
|
* the AWT listener model. |
|
* <p> |
|
* A {@link javax.swing.text.Keymap} lets an application bind key |
|
* strokes to actions. |
|
* In order to allow keymaps to be shared across multiple text components, they |
|
* can use actions that extend <code>TextAction</code>. |
|
* <code>TextAction</code> can determine which <code>JTextComponent</code> |
|
* most recently has or had focus and therefore is the subject of |
|
* the action (In the case that the <code>ActionEvent</code> |
|
* sent to the action doesn't contain the target text component as its source). |
|
* <p> |
|
* The <a href="../../../../technotes/guides/imf/spec.html">input method framework</a> |
|
* lets text components interact with input methods, separate software |
|
* components that preprocess events to let users enter thousands of |
|
* different characters using keyboards with far fewer keys. |
|
* <code>JTextComponent</code> is an <em>active client</em> of |
|
* the framework, so it implements the preferred user interface for interacting |
|
* with input methods. As a consequence, some key events do not reach the text |
|
* component because they are handled by an input method, and some text input |
|
* reaches the text component as committed text within an {@link |
|
* java.awt.event.InputMethodEvent} instead of as a key event. |
|
* The complete text input is the combination of the characters in |
|
* <code>keyTyped</code> key events and committed text in input method events. |
|
* <p> |
|
* The AWT listener model lets applications attach event listeners to |
|
* components in order to bind events to actions. Swing encourages the |
|
* use of keymaps instead of listeners, but maintains compatibility |
|
* with listeners by giving the listeners a chance to steal an event |
|
* by consuming it. |
|
* <p> |
|
* Keyboard event and input method events are handled in the following stages, |
|
* with each stage capable of consuming the event: |
|
* |
|
* <table border=1 summary="Stages of keyboard and input method event handling"> |
|
* <tr> |
|
* <th id="stage"><p style="text-align:left">Stage</p></th> |
|
* <th id="ke"><p style="text-align:left">KeyEvent</p></th> |
|
* <th id="ime"><p style="text-align:left">InputMethodEvent</p></th></tr> |
|
* <tr><td headers="stage">1. </td> |
|
* <td headers="ke">input methods </td> |
|
* <td headers="ime">(generated here)</td></tr> |
|
* <tr><td headers="stage">2. </td> |
|
* <td headers="ke">focus manager </td> |
|
* <td headers="ime"></td> |
|
* </tr> |
|
* <tr> |
|
* <td headers="stage">3. </td> |
|
* <td headers="ke">registered key listeners</td> |
|
* <td headers="ime">registered input method listeners</tr> |
|
* <tr> |
|
* <td headers="stage">4. </td> |
|
* <td headers="ke"></td> |
|
* <td headers="ime">input method handling in JTextComponent</tr> |
|
* <tr> |
|
* <td headers="stage">5. </td><td headers="ke ime" colspan=2>keymap handling using the current keymap</td></tr> |
|
* <tr><td headers="stage">6. </td><td headers="ke">keyboard handling in JComponent (e.g. accelerators, component navigation, etc.)</td> |
|
* <td headers="ime"></td></tr> |
|
* </table> |
|
* |
|
* <p> |
|
* To maintain compatibility with applications that listen to key |
|
* events but are not aware of input method events, the input |
|
* method handling in stage 4 provides a compatibility mode for |
|
* components that do not process input method events. For these |
|
* components, the committed text is converted to keyTyped key events |
|
* and processed in the key event pipeline starting at stage 3 |
|
* instead of in the input method event pipeline. |
|
* <p> |
|
* By default the component will create a keymap (named <b>DEFAULT_KEYMAP</b>) |
|
* that is shared by all JTextComponent instances as the default keymap. |
|
* Typically a look-and-feel implementation will install a different keymap |
|
* that resolves to the default keymap for those bindings not found in the |
|
* different keymap. The minimal bindings include: |
|
* <ul> |
|
* <li>inserting content into the editor for the |
|
* printable keys. |
|
* <li>removing content with the backspace and del |
|
* keys. |
|
* <li>caret movement forward and backward |
|
* </ul> |
|
* |
|
* <dt><b><font size=+1>Model/View Split</font></b> |
|
* <dd> |
|
* The text components have a model-view split. A text component pulls |
|
* together the objects used to represent the model, view, and controller. |
|
* The text document model may be shared by other views which act as observers |
|
* of the model (e.g. a document may be shared by multiple components). |
|
* |
|
* <p style="text-align:center"><img src="doc-files/editor.gif" alt="Diagram showing interaction between Controller, Document, events, and ViewFactory" |
|
* HEIGHT=358 WIDTH=587></p> |
|
* |
|
* <p> |
|
* The model is defined by the {@link Document} interface. |
|
* This is intended to provide a flexible text storage mechanism |
|
* that tracks change during edits and can be extended to more sophisticated |
|
* models. The model interfaces are meant to capture the capabilities of |
|
* expression given by SGML, a system used to express a wide variety of |
|
* content. |
|
* Each modification to the document causes notification of the |
|
* details of the change to be sent to all observers in the form of a |
|
* {@link DocumentEvent} which allows the views to stay up to date with the model. |
|
* This event is sent to observers that have implemented the |
|
* {@link DocumentListener} |
|
* interface and registered interest with the model being observed. |
|
* |
|
* <dt><b><font size=+1>Location Information</font></b> |
|
* <dd> |
|
* The capability of determining the location of text in |
|
* the view is provided. There are two methods, {@link #modelToView} |
|
* and {@link #viewToModel} for determining this information. |
|
* |
|
* <dt><b><font size=+1>Undo/Redo support</font></b> |
|
* <dd> |
|
* Support for an edit history mechanism is provided to allow |
|
* undo/redo operations. The text component does not itself |
|
* provide the history buffer by default, but does provide |
|
* the <code>UndoableEdit</code> records that can be used in conjunction |
|
* with a history buffer to provide the undo/redo support. |
|
* The support is provided by the Document model, which allows |
|
* one to attach UndoableEditListener implementations. |
|
* |
|
* <dt><b><font size=+1>Thread Safety</font></b> |
|
* <dd> |
|
* The swing text components provide some support of thread |
|
* safe operations. Because of the high level of configurability |
|
* of the text components, it is possible to circumvent the |
|
* protection provided. The protection primarily comes from |
|
* the model, so the documentation of <code>AbstractDocument</code> |
|
* describes the assumptions of the protection provided. |
|
* The methods that are safe to call asynchronously are marked |
|
* with comments. |
|
* |
|
* <dt><b><font size=+1>Newlines</font></b> |
|
* <dd> |
|
* For a discussion on how newlines are handled, see |
|
* <a href="DefaultEditorKit.html">DefaultEditorKit</a>. |
|
* |
|
* |
|
* <dt><b><font size=+1>Printing support</font></b> |
|
* <dd> |
|
* Several {@link #print print} methods are provided for basic |
|
* document printing. If more advanced printing is needed, use the |
|
* {@link #getPrintable} method. |
|
* </dl> |
|
* |
|
* <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™ |
|
* has been added to the <code>java.beans</code> package. |
|
* Please see {@link java.beans.XMLEncoder}. |
|
* |
|
* @beaninfo |
|
* attribute: isContainer false |
|
* |
|
* @author Timothy Prinzing |
|
* @author Igor Kushnirskiy (printing support) |
|
* @see Document |
|
* @see DocumentEvent |
|
* @see DocumentListener |
|
* @see Caret |
|
* @see CaretEvent |
|
* @see CaretListener |
|
* @see TextUI |
|
* @see View |
|
* @see ViewFactory |
|
*/ |
|
public abstract class JTextComponent extends JComponent implements Scrollable, Accessible |
|
{ |
|
/** |
|
* Creates a new <code>JTextComponent</code>. |
|
* Listeners for caret events are established, and the pluggable |
|
* UI installed. The component is marked as editable. No layout manager |
|
* is used, because layout is managed by the view subsystem of text. |
|
* The document model is set to <code>null</code>. |
|
*/ |
|
public JTextComponent() { |
|
super(); |
|
// enable InputMethodEvent for on-the-spot pre-editing |
|
enableEvents(AWTEvent.KEY_EVENT_MASK | AWTEvent.INPUT_METHOD_EVENT_MASK); |
|
caretEvent = new MutableCaretEvent(this); |
|
addMouseListener(caretEvent); |
|
addFocusListener(caretEvent); |
|
setEditable(true); |
|
setDragEnabled(false); |
|
setLayout(null); // layout is managed by View hierarchy |
|
updateUI(); |
|
} |
|
/** |
|
* Fetches the user-interface factory for this text-oriented editor. |
|
* |
|
* @return the factory |
|
*/ |
|
public TextUI getUI() { return (TextUI)ui; } |
|
/** |
|
* Sets the user-interface factory for this text-oriented editor. |
|
* |
|
* @param ui the factory |
|
*/ |
|
public void setUI(TextUI ui) { |
|
super.setUI(ui); |
|
} |
|
/** |
|
* Reloads the pluggable UI. The key used to fetch the |
|
* new interface is <code>getUIClassID()</code>. The type of |
|
* the UI is <code>TextUI</code>. <code>invalidate</code> |
|
* is called after setting the UI. |
|
*/ |
|
public void updateUI() { |
|
setUI((TextUI)UIManager.getUI(this)); |
|
invalidate(); |
|
} |
|
/** |
|
* Adds a caret listener for notification of any changes |
|
* to the caret. |
|
* |
|
* @param listener the listener to be added |
|
* @see javax.swing.event.CaretEvent |
|
*/ |
|
public void addCaretListener(CaretListener listener) { |
|
listenerList.add(CaretListener.class, listener); |
|
} |
|
/** |
|
* Removes a caret listener. |
|
* |
|
* @param listener the listener to be removed |
|
* @see javax.swing.event.CaretEvent |
|
*/ |
|
public void removeCaretListener(CaretListener listener) { |
|
listenerList.remove(CaretListener.class, listener); |
|
} |
|
/** |
|
* Returns an array of all the caret listeners |
|
* registered on this text component. |
|
* |
|
* @return all of this component's <code>CaretListener</code>s |
|
* or an empty |
|
* array if no caret listeners are currently registered |
|
* |
|
* @see #addCaretListener |
|
* @see #removeCaretListener |
|
* |
|
* @since 1.4 |
|
*/ |
|
public CaretListener[] getCaretListeners() { |
|
return listenerList.getListeners(CaretListener.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 in a |
|
* last-to-first manner. |
|
* |
|
* @param e the event |
|
* @see EventListenerList |
|
*/ |
|
protected void fireCaretUpdate(CaretEvent e) { |
|
// 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]==CaretListener.class) { |
|
((CaretListener)listeners[i+1]).caretUpdate(e); |
|
} |
|
} |
|
} |
|
/** |
|
* Associates the editor with a text document. |
|
* The currently registered factory is used to build a view for |
|
* the document, which gets displayed by the editor after revalidation. |
|
* A PropertyChange event ("document") is propagated to each listener. |
|
* |
|
* @param doc the document to display/edit |
|
* @see #getDocument |
|
* @beaninfo |
|
* description: the text document model |
|
* bound: true |
|
* expert: true |
|
*/ |
|
public void setDocument(Document doc) { |
|
Document old = model; |
|
/* |
|
* acquire a read lock on the old model to prevent notification of |
|
* mutations while we disconnecting the old model. |
|
*/ |
|
try { |
|
if (old instanceof AbstractDocument) { |
|
((AbstractDocument)old).readLock(); |
|
} |
|
if (accessibleContext != null) { |
|
model.removeDocumentListener( |
|
((AccessibleJTextComponent)accessibleContext)); |
|
} |
|
if (inputMethodRequestsHandler != null) { |
|
model.removeDocumentListener((DocumentListener)inputMethodRequestsHandler); |
|
} |
|
model = doc; |
|
// Set the document's run direction property to match the |
|
// component's ComponentOrientation property. |
|
Boolean runDir = getComponentOrientation().isLeftToRight() |
|
? TextAttribute.RUN_DIRECTION_LTR |
|
: TextAttribute.RUN_DIRECTION_RTL; |
|
if (runDir != doc.getProperty(TextAttribute.RUN_DIRECTION)) { |
|
doc.putProperty(TextAttribute.RUN_DIRECTION, runDir ); |
|
} |
|
firePropertyChange("document", old, doc); |
|
} finally { |
|
if (old instanceof AbstractDocument) { |
|
((AbstractDocument)old).readUnlock(); |
|
} |
|
} |
|
revalidate(); |
|
repaint(); |
|
if (accessibleContext != null) { |
|
model.addDocumentListener( |
|
((AccessibleJTextComponent)accessibleContext)); |
|
} |
|
if (inputMethodRequestsHandler != null) { |
|
model.addDocumentListener((DocumentListener)inputMethodRequestsHandler); |
|
} |
|
} |
|
/** |
|
* Fetches the model associated with the editor. This is |
|
* primarily for the UI to get at the minimal amount of |
|
* state required to be a text editor. Subclasses will |
|
* return the actual type of the model which will typically |
|
* be something that extends Document. |
|
* |
|
* @return the model |
|
*/ |
|
public Document getDocument() { |
|
return model; |
|
} |
|
// Override of Component.setComponentOrientation |
|
public void setComponentOrientation( ComponentOrientation o ) { |
|
// Set the document's run direction property to match the |
|
// ComponentOrientation property. |
|
Document doc = getDocument(); |
|
if( doc != null ) { |
|
Boolean runDir = o.isLeftToRight() |
|
? TextAttribute.RUN_DIRECTION_LTR |
|
: TextAttribute.RUN_DIRECTION_RTL; |
|
doc.putProperty( TextAttribute.RUN_DIRECTION, runDir ); |
|
} |
|
super.setComponentOrientation( o ); |
|
} |
|
/** |
|
* Fetches the command list for the editor. This is |
|
* the list of commands supported by the plugged-in UI |
|
* augmented by the collection of commands that the |
|
* editor itself supports. These are useful for binding |
|
* to events, such as in a keymap. |
|
* |
|
* @return the command list |
|
*/ |
|
public Action[] getActions() { |
|
return getUI().getEditorKit(this).getActions(); |
|
} |
|
/** |
|
* Sets margin space between the text component's border |
|
* and its text. The text component's default <code>Border</code> |
|
* object will use this value to create the proper margin. |
|
* However, if a non-default border is set on the text component, |
|
* it is that <code>Border</code> object's responsibility to create the |
|
* appropriate margin space (else this property will effectively |
|
* be ignored). This causes a redraw of the component. |
|
* A PropertyChange event ("margin") is sent to all listeners. |
|
* |
|
* @param m the space between the border and the text |
|
* @beaninfo |
|
* description: desired space between the border and text area |
|
* bound: true |
|
*/ |
|
public void setMargin(Insets m) { |
|
Insets old = margin; |
|
margin = m; |
|
firePropertyChange("margin", old, m); |
|
invalidate(); |
|
} |
|
/** |
|
* Returns the margin between the text component's border and |
|
* its text. |
|
* |
|
* @return the margin |
|
*/ |
|
public Insets getMargin() { |
|
return margin; |
|
} |
|
/** |
|
* Sets the <code>NavigationFilter</code>. <code>NavigationFilter</code> |
|
* is used by <code>DefaultCaret</code> and the default cursor movement |
|
* actions as a way to restrict the cursor movement. |
|
* |
|
* @since 1.4 |
|
*/ |
|
public void setNavigationFilter(NavigationFilter filter) { |
|
navigationFilter = filter; |
|
} |
|
/** |
|
* Returns the <code>NavigationFilter</code>. <code>NavigationFilter</code> |
|
* is used by <code>DefaultCaret</code> and the default cursor movement |
|
* actions as a way to restrict the cursor movement. A null return value |
|
* implies the cursor movement and selection should not be restricted. |
|
* |
|
* @since 1.4 |
|
* @return the NavigationFilter |
|
*/ |
|
public NavigationFilter getNavigationFilter() { |
|
return navigationFilter; |
|
} |
|
/** |
|
* Fetches the caret that allows text-oriented navigation over |
|
* the view. |
|
* |
|
* @return the caret |
|
*/ |
|
@Transient |
|
public Caret getCaret() { |
|
return caret; |
|
} |
|
/** |
|
* Sets the caret to be used. By default this will be set |
|
* by the UI that gets installed. This can be changed to |
|
* a custom caret if desired. Setting the caret results in a |
|
* PropertyChange event ("caret") being fired. |
|
* |
|
* @param c the caret |
|
* @see #getCaret |
|
* @beaninfo |
|
* description: the caret used to select/navigate |
|
* bound: true |
|
* expert: true |
|
*/ |
|
public void setCaret(Caret c) { |
|
if (caret != null) { |
|
caret.removeChangeListener(caretEvent); |
|
caret.deinstall(this); |
|
} |
|
Caret old = caret; |
|
caret = c; |
|
if (caret != null) { |
|
caret.install(this); |
|
caret.addChangeListener(caretEvent); |
|
} |
|
firePropertyChange("caret", old, caret); |
|
} |
|
/** |
|
* Fetches the object responsible for making highlights. |
|
* |
|
* @return the highlighter |
|
*/ |
|
public Highlighter getHighlighter() { |
|
return highlighter; |
|
} |
|
/** |
|
* Sets the highlighter to be used. By default this will be set |
|
* by the UI that gets installed. This can be changed to |
|
* a custom highlighter if desired. The highlighter can be set to |
|
* <code>null</code> to disable it. |
|
* A PropertyChange event ("highlighter") is fired |
|
* when a new highlighter is installed. |
|
* |
|
* @param h the highlighter |
|
* @see #getHighlighter |
|
* @beaninfo |
|
* description: object responsible for background highlights |
|
* bound: true |
|
* expert: true |
|
*/ |
|
public void setHighlighter(Highlighter h) { |
|
if (highlighter != null) { |
|
highlighter.deinstall(this); |
|
} |
|
Highlighter old = highlighter; |
|
highlighter = h; |
|
if (highlighter != null) { |
|
highlighter.install(this); |
|
} |
|
firePropertyChange("highlighter", old, h); |
|
} |
|
/** |
|
* Sets the keymap to use for binding events to |
|
* actions. Setting to <code>null</code> effectively disables |
|
* keyboard input. |
|
* A PropertyChange event ("keymap") is fired when a new keymap |
|
* is installed. |
|
* |
|
* @param map the keymap |
|
* @see #getKeymap |
|
* @beaninfo |
|
* description: set of key event to action bindings to use |
|
* bound: true |
|
*/ |
|
public void setKeymap(Keymap map) { |
|
Keymap old = keymap; |
|
keymap = map; |
|
firePropertyChange("keymap", old, keymap); |
|
updateInputMap(old, map); |
|
} |
|
/** |
|
* Turns on or off automatic drag handling. In order to enable automatic |
|
* drag handling, this property should be set to {@code true}, and the |
|
* component's {@code TransferHandler} needs to be {@code non-null}. |
|
* The default value of the {@code dragEnabled} property is {@code false}. |
|
* <p> |
|
* The job of honoring this property, and recognizing a user drag gesture, |
|
* lies with the look and feel implementation, and in particular, the component's |
|
* {@code TextUI}. When automatic drag handling is enabled, most look and |
|
* feels (including those that subclass {@code BasicLookAndFeel}) begin a |
|
* drag and drop operation whenever the user presses the mouse button over |
|
* a selection and then moves the mouse a few pixels. Setting this property to |
|
* {@code true} can therefore have a subtle effect on how selections behave. |
|
* <p> |
|
* If a look and feel is used that ignores this property, you can still |
|
* begin a drag and drop operation by calling {@code exportAsDrag} on the |
|
* component's {@code TransferHandler}. |
|
* |
|
* @param b whether or not to enable automatic drag handling |
|
* @exception HeadlessException if |
|
* <code>b</code> is <code>true</code> and |
|
* <code>GraphicsEnvironment.isHeadless()</code> |
|
* returns <code>true</code> |
|
* @see java.awt.GraphicsEnvironment#isHeadless |
|
* @see #getDragEnabled |
|
* @see #setTransferHandler |
|
* @see TransferHandler |
|
* @since 1.4 |
|
* |
|
* @beaninfo |
|
* description: determines whether automatic drag handling is enabled |
|
* bound: false |
|
*/ |
|
public void setDragEnabled(boolean b) { |
|
if (b && GraphicsEnvironment.isHeadless()) { |
|
throw new HeadlessException(); |
|
} |
|
dragEnabled = b; |
|
} |
|
/** |
|
* Returns whether or not automatic drag handling is enabled. |
|
* |
|
* @return the value of the {@code dragEnabled} property |
|
* @see #setDragEnabled |
|
* @since 1.4 |
|
*/ |
|
public boolean getDragEnabled() { |
|
return dragEnabled; |
|
} |
|
/** |
|
* Sets the drop mode for this component. For backward compatibility, |
|
* the default for this property is <code>DropMode.USE_SELECTION</code>. |
|
* Usage of <code>DropMode.INSERT</code> is recommended, however, |
|
* for an improved user experience. It offers similar behavior of dropping |
|
* between text locations, but does so without affecting the actual text |
|
* selection and caret location. |
|
* <p> |
|
* <code>JTextComponents</code> support the following drop modes: |
|
* <ul> |
|
* <li><code>DropMode.USE_SELECTION</code></li> |
|
* <li><code>DropMode.INSERT</code></li> |
|
* </ul> |
|
* <p> |
|
* The drop mode is only meaningful if this component has a |
|
* <code>TransferHandler</code> that accepts drops. |
|
* |
|
* @param dropMode the drop mode to use |
|
* @throws IllegalArgumentException if the drop mode is unsupported |
|
* or <code>null</code> |
|
* @see #getDropMode |
|
* @see #getDropLocation |
|
* @see #setTransferHandler |
|
* @see javax.swing.TransferHandler |
|
* @since 1.6 |
|
*/ |
|
public final void setDropMode(DropMode dropMode) { |
|
if (dropMode != null) { |
|
switch (dropMode) { |
|
case USE_SELECTION: |
|
case INSERT: |
|
this.dropMode = dropMode; |
|
return; |
|
} |
|
} |
|
throw new IllegalArgumentException(dropMode + ": Unsupported drop mode for text"); |
|
} |
|
/** |
|
* Returns the drop mode for this component. |
|
* |
|
* @return the drop mode for this component |
|
* @see #setDropMode |
|
* @since 1.6 |
|
*/ |
|
public final DropMode getDropMode() { |
|
return dropMode; |
|
} |
|
static { |
|
SwingAccessor.setJTextComponentAccessor( |
|
new SwingAccessor.JTextComponentAccessor() { |
|
public TransferHandler.DropLocation dropLocationForPoint(JTextComponent textComp, |
|
Point p) |
|
{ |
|
return textComp.dropLocationForPoint(p); |
|
} |
|
public Object setDropLocation(JTextComponent textComp, |
|
TransferHandler.DropLocation location, |
|
Object state, boolean forDrop) |
|
{ |
|
return textComp.setDropLocation(location, state, forDrop); |
|
} |
|
}); |
|
} |
|
/** |
|
* Calculates a drop location in this component, representing where a |
|
* drop at the given point should insert data. |
|
* <p> |
|
* Note: This method is meant to override |
|
* <code>JComponent.dropLocationForPoint()</code>, which is package-private |
|
* in javax.swing. <code>TransferHandler</code> will detect text components |
|
* and call this method instead via reflection. It's name should therefore |
|
* not be changed. |
|
* |
|
* @param p the point to calculate a drop location for |
|
* @return the drop location, or <code>null</code> |
|
*/ |
|
DropLocation dropLocationForPoint(Point p) { |
|
Position.Bias[] bias = new Position.Bias[1]; |
|
int index = getUI().viewToModel(this, p, bias); |
|
// viewToModel currently returns null for some HTML content |
|
// when the point is within the component's top inset |
|
if (bias[0] == null) { |
|
bias[0] = Position.Bias.Forward; |
|
} |
|
return new DropLocation(p, index, bias[0]); |
|
} |
|
/** |
|
* Called to set or clear the drop location during a DnD operation. |
|
* In some cases, the component may need to use it's internal selection |
|
* temporarily to indicate the drop location. To help facilitate this, |
|
* this method returns and accepts as a parameter a state object. |
|
* This state object can be used to store, and later restore, the selection |
|
* state. Whatever this method returns will be passed back to it in |
|
* future calls, as the state parameter. If it wants the DnD system to |
|
* continue storing the same state, it must pass it back every time. |
|
* Here's how this is used: |
|
* <p> |
|
* Let's say that on the first call to this method the component decides |
|
* to save some state (because it is about to use the selection to show |
|
* a drop index). It can return a state object to the caller encapsulating |
|
* any saved selection state. On a second call, let's say the drop location |
|
* is being changed to something else. The component doesn't need to |
|
* restore anything yet, so it simply passes back the same state object |
|
* to have the DnD system continue storing it. Finally, let's say this |
|
* method is messaged with <code>null</code>. This means DnD |
|
* is finished with this component for now, meaning it should restore |
|
* state. At this point, it can use the state parameter to restore |
|
* said state, and of course return <code>null</code> since there's |
|
* no longer anything to store. |
|
* <p> |
|
* Note: This method is meant to override |
|
* <code>JComponent.setDropLocation()</code>, which is package-private |
|
* in javax.swing. <code>TransferHandler</code> will detect text components |
|
* and call this method instead via reflection. It's name should therefore |
|
* not be changed. |
|
* |
|
* @param location the drop location (as calculated by |
|
* <code>dropLocationForPoint</code>) or <code>null</code> |
|
* if there's no longer a valid drop location |
|
* @param state the state object saved earlier for this component, |
|
* or <code>null</code> |
|
* @param forDrop whether or not the method is being called because an |
|
* actual drop occurred |
|
* @return any saved state for this component, or <code>null</code> if none |
|
*/ |
|
Object setDropLocation(TransferHandler.DropLocation location, |
|
Object state, |
|
boolean forDrop) { |
|
Object retVal = null; |
|
DropLocation textLocation = (DropLocation)location; |
|
if (dropMode == DropMode.USE_SELECTION) { |
|
if (textLocation == null) { |
|
if (state != null) { |
|
/* |
|
* This object represents the state saved earlier. |
|
* If the caret is a DefaultCaret it will be |
|
* an Object array containing, in order: |
|
* - the saved caret mark (Integer) |
|
* - the saved caret dot (Integer) |
|
* - the saved caret visibility (Boolean) |
|
* - the saved mark bias (Position.Bias) |
|
* - the saved dot bias (Position.Bias) |
|
* If the caret is not a DefaultCaret it will |
|
* be similar, but will not contain the dot |
|
* or mark bias. |
|
*/ |
|
Object[] vals = (Object[])state; |
|
if (!forDrop) { |
|
if (caret instanceof DefaultCaret) { |
|
((DefaultCaret)caret).setDot(((Integer)vals[0]).intValue(), |
|
(Position.Bias)vals[3]); |
|
((DefaultCaret)caret).moveDot(((Integer)vals[1]).intValue(), |
|
(Position.Bias)vals[4]); |
|
} else { |
|
caret.setDot(((Integer)vals[0]).intValue()); |
|
caret.moveDot(((Integer)vals[1]).intValue()); |
|
} |
|
} |
|
caret.setVisible(((Boolean)vals[2]).booleanValue()); |
|
} |
|
} else { |
|
if (dropLocation == null) { |
|
boolean visible; |
|
if (caret instanceof DefaultCaret) { |
|
DefaultCaret dc = (DefaultCaret)caret; |
|
visible = dc.isActive(); |
|
retVal = new Object[] {Integer.valueOf(dc.getMark()), |
|
Integer.valueOf(dc.getDot()), |
|
Boolean.valueOf(visible), |
|
dc.getMarkBias(), |
|
dc.getDotBias()}; |
|
} else { |
|
visible = caret.isVisible(); |
|
retVal = new Object[] {Integer.valueOf(caret.getMark()), |
|
Integer.valueOf(caret.getDot()), |
|
Boolean.valueOf(visible)}; |
|
} |
|
caret.setVisible(true); |
|
} else { |
|
retVal = state; |
|
} |
|
if (caret instanceof DefaultCaret) { |
|
((DefaultCaret)caret).setDot(textLocation.getIndex(), textLocation.getBias()); |
|
} else { |
|
caret.setDot(textLocation.getIndex()); |
|
} |
|
} |
|
} else { |
|
if (textLocation == null) { |
|
if (state != null) { |
|
caret.setVisible(((Boolean)state).booleanValue()); |
|
} |
|
} else { |
|
if (dropLocation == null) { |
|
boolean visible = caret instanceof DefaultCaret |
|
? ((DefaultCaret)caret).isActive() |
|
: caret.isVisible(); |
|
retVal = Boolean.valueOf(visible); |
|
caret.setVisible(false); |
|
} else { |
|
retVal = state; |
|
} |
|
} |
|
} |
|
DropLocation old = dropLocation; |
|
dropLocation = textLocation; |
|
firePropertyChange("dropLocation", old, dropLocation); |
|
return retVal; |
|
} |
|
/** |
|
* Returns the location that this component should visually indicate |
|
* as the drop location during a DnD operation over the component, |
|
* or {@code null} if no location is to currently be shown. |
|
* <p> |
|
* This method is not meant for querying the drop location |
|
* from a {@code TransferHandler}, as the drop location is only |
|
* set after the {@code TransferHandler}'s <code>canImport</code> |
|
* has returned and has allowed for the location to be shown. |
|
* <p> |
|
* When this property changes, a property change event with |
|
* name "dropLocation" is fired by the component. |
|
* |
|
* @return the drop location |
|
* @see #setDropMode |
|
* @see TransferHandler#canImport(TransferHandler.TransferSupport) |
|
* @since 1.6 |
|
*/ |
|
public final DropLocation getDropLocation() { |
|
return dropLocation; |
|
} |
|
/** |
|
* Updates the <code>InputMap</code>s in response to a |
|
* <code>Keymap</code> change. |
|
* @param oldKm the old <code>Keymap</code> |
|
* @param newKm the new <code>Keymap</code> |
|
*/ |
|
void updateInputMap(Keymap oldKm, Keymap newKm) { |
|
// Locate the current KeymapWrapper. |
|
InputMap km = getInputMap(JComponent.WHEN_FOCUSED); |
|
InputMap last = km; |
|
while (km != null && !(km instanceof KeymapWrapper)) { |
|
last = km; |
|
km = km.getParent(); |
|
} |
|
if (km != null) { |
|
// Found it, tweak the InputMap that points to it, as well |
|
// as anything it points to. |
|
if (newKm == null) { |
|
if (last != km) { |
|
last.setParent(km.getParent()); |
|
} |
|
else { |
|
last.setParent(null); |
|
} |
|
} |
|
else { |
|
InputMap newKM = new KeymapWrapper(newKm); |
|
last.setParent(newKM); |
|
if (last != km) { |
|
newKM.setParent(km.getParent()); |
|
} |
|
} |
|
} |
|
else if (newKm != null) { |
|
km = getInputMap(JComponent.WHEN_FOCUSED); |
|
if (km != null) { |
|
// Couldn't find it. |
|
// Set the parent of WHEN_FOCUSED InputMap to be the new one. |
|
InputMap newKM = new KeymapWrapper(newKm); |
|
newKM.setParent(km.getParent()); |
|
km.setParent(newKM); |
|
} |
|
} |
|
// Do the same thing with the ActionMap |
|
ActionMap am = getActionMap(); |
|
ActionMap lastAM = am; |
|
while (am != null && !(am instanceof KeymapActionMap)) { |
|
lastAM = am; |
|
am = am.getParent(); |
|
} |
|
if (am != null) { |
|
// Found it, tweak the Actionap that points to it, as well |
|
// as anything it points to. |
|
if (newKm == null) { |
|
if (lastAM != am) { |
|
lastAM.setParent(am.getParent()); |
|
} |
|
else { |
|
lastAM.setParent(null); |
|
} |
|
} |
|
else { |
|
ActionMap newAM = new KeymapActionMap(newKm); |
|
lastAM.setParent(newAM); |
|
if (lastAM != am) { |
|
newAM.setParent(am.getParent()); |
|
} |
|
} |
|
} |
|
else if (newKm != null) { |
|
am = getActionMap(); |
|
if (am != null) { |
|
// Couldn't find it. |
|
// Set the parent of ActionMap to be the new one. |
|
ActionMap newAM = new KeymapActionMap(newKm); |
|
newAM.setParent(am.getParent()); |
|
am.setParent(newAM); |
|
} |
|
} |
|
} |
|
/** |
|
* Fetches the keymap currently active in this text |
|
* component. |
|
* |
|
* @return the keymap |
|
*/ |
|
public Keymap getKeymap() { |
|
return keymap; |
|
} |
|
/** |
|
* Adds a new keymap into the keymap hierarchy. Keymap bindings |
|
* resolve from bottom up so an attribute specified in a child |
|
* will override an attribute specified in the parent. |
|
* |
|
* @param nm the name of the keymap (must be unique within the |
|
* collection of named keymaps in the document); the name may |
|
* be <code>null</code> if the keymap is unnamed, |
|
* but the caller is responsible for managing the reference |
|
* returned as an unnamed keymap can't |
|
* be fetched by name |
|
* @param parent the parent keymap; this may be <code>null</code> if |
|
* unspecified bindings need not be resolved in some other keymap |
|
* @return the keymap |
|
*/ |
|
public static Keymap addKeymap(String nm, Keymap parent) { |
|
Keymap map = new DefaultKeymap(nm, parent); |
|
if (nm != null) { |
|
// add a named keymap, a class of bindings |
|
getKeymapTable().put(nm, map); |
|
} |
|
return map; |
|
} |
|
/** |
|
* Removes a named keymap previously added to the document. Keymaps |
|
* with <code>null</code> names may not be removed in this way. |
|
* |
|
* @param nm the name of the keymap to remove |
|
* @return the keymap that was removed |
|
*/ |
|
public static Keymap removeKeymap(String nm) { |
|
return getKeymapTable().remove(nm); |
|
} |
|
/** |
|
* Fetches a named keymap previously added to the document. |
|
* This does not work with <code>null</code>-named keymaps. |
|
* |
|
* @param nm the name of the keymap |
|
* @return the keymap |
|
*/ |
|
public static Keymap getKeymap(String nm) { |
|
return getKeymapTable().get(nm); |
|
} |
|
private static HashMap<String,Keymap> getKeymapTable() { |
|
synchronized (KEYMAP_TABLE) { |
|
AppContext appContext = AppContext.getAppContext(); |
|
HashMap<String,Keymap> keymapTable = |
|
(HashMap<String,Keymap>)appContext.get(KEYMAP_TABLE); |
|
if (keymapTable == null) { |
|
keymapTable = new HashMap<String,Keymap>(17); |
|
appContext.put(KEYMAP_TABLE, keymapTable); |
|
//initialize default keymap |
|
Keymap binding = addKeymap(DEFAULT_KEYMAP, null); |
|
binding.setDefaultAction(new |
|
DefaultEditorKit.DefaultKeyTypedAction()); |
|
} |
|
return keymapTable; |
|
} |
|
} |
|
/** |
|
* Binding record for creating key bindings. |
|
* <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™ |
|
* has been added to the <code>java.beans</code> package. |
|
* Please see {@link java.beans.XMLEncoder}. |
|
*/ |
|
public static class KeyBinding { |
|
/** |
|
* The key. |
|
*/ |
|
public KeyStroke key; |
|
/** |
|
* The name of the action for the key. |
|
*/ |
|
public String actionName; |
|
/** |
|
* Creates a new key binding. |
|
* |
|
* @param key the key |
|
* @param actionName the name of the action for the key |
|
*/ |
|
public KeyBinding(KeyStroke key, String actionName) { |
|
this.key = key; |
|
this.actionName = actionName; |
|
} |
|
} |
|
/** |
|
* <p> |
|
* Loads a keymap with a bunch of |
|
* bindings. This can be used to take a static table of |
|
* definitions and load them into some keymap. The following |
|
* example illustrates an example of binding some keys to |
|
* the cut, copy, and paste actions associated with a |
|
* JTextComponent. A code fragment to accomplish |
|
* this might look as follows: |
|
* <pre><code> |
|
* |
|
* static final JTextComponent.KeyBinding[] defaultBindings = { |
|
* new JTextComponent.KeyBinding( |
|
* KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_MASK), |
|
* DefaultEditorKit.copyAction), |
|
* new JTextComponent.KeyBinding( |
|
* KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_MASK), |
|
* DefaultEditorKit.pasteAction), |
|
* new JTextComponent.KeyBinding( |
|
* KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_MASK), |
|
* DefaultEditorKit.cutAction), |
|
* }; |
|
* |
|
* JTextComponent c = new JTextPane(); |
|
* Keymap k = c.getKeymap(); |
|
* JTextComponent.loadKeymap(k, defaultBindings, c.getActions()); |
|
* |
|
* </code></pre> |
|
* The sets of bindings and actions may be empty but must be |
|
* non-<code>null</code>. |
|
* |
|
* @param map the keymap |
|
* @param bindings the bindings |
|
* @param actions the set of actions |
|
*/ |
|
public static void loadKeymap(Keymap map, KeyBinding[] bindings, Action[] actions) { |
|
Hashtable<String, Action> h = new Hashtable<String, Action>(); |
|
for (Action a : actions) { |
|
String value = (String)a.getValue(Action.NAME); |
|
h.put((value!=null ? value:""), a); |
|
} |
|
for (KeyBinding binding : bindings) { |
|
Action a = h.get(binding.actionName); |
|
if (a != null) { |
|
map.addActionForKeyStroke(binding.key, a); |
|
} |
|
} |
|
} |
|
/** |
|
* Fetches the current color used to render the |
|
* caret. |
|
* |
|
* @return the color |
|
*/ |
|
public Color getCaretColor() { |
|
return caretColor; |
|
} |
|
/** |
|
* Sets the current color used to render the caret. |
|
* Setting to <code>null</code> effectively restores the default color. |
|
* Setting the color results in a PropertyChange event ("caretColor") |
|
* being fired. |
|
* |
|
* @param c the color |
|
* @see #getCaretColor |
|
* @beaninfo |
|
* description: the color used to render the caret |
|
* bound: true |
|
* preferred: true |
|
*/ |
|
public void setCaretColor(Color c) { |
|
Color old = caretColor; |
|
caretColor = c; |
|
firePropertyChange("caretColor", old, caretColor); |
|
} |
|
/** |
|
* Fetches the current color used to render the |
|
* selection. |
|
* |
|
* @return the color |
|
*/ |
|
public Color getSelectionColor() { |
|
return selectionColor; |
|
} |
|
/** |
|
* Sets the current color used to render the selection. |
|
* Setting the color to <code>null</code> is the same as setting |
|
* <code>Color.white</code>. Setting the color results in a |
|
* PropertyChange event ("selectionColor"). |
|
* |
|
* @param c the color |
|
* @see #getSelectionColor |
|
* @beaninfo |
|
* description: color used to render selection background |
|
* bound: true |
|
* preferred: true |
|
*/ |
|
public void setSelectionColor(Color c) { |
|
Color old = selectionColor; |
|
selectionColor = c; |
|
firePropertyChange("selectionColor", old, selectionColor); |
|
} |
|
/** |
|
* Fetches the current color used to render the |
|
* selected text. |
|
* |
|
* @return the color |
|
*/ |
|
public Color getSelectedTextColor() { |
|
return selectedTextColor; |
|
} |
|
/** |
|
* Sets the current color used to render the selected text. |
|
* Setting the color to <code>null</code> is the same as |
|
* <code>Color.black</code>. Setting the color results in a |
|
* PropertyChange event ("selectedTextColor") being fired. |
|
* |
|
* @param c the color |
|
* @see #getSelectedTextColor |
|
* @beaninfo |
|
* description: color used to render selected text |
|
* bound: true |
|
* preferred: true |
|
*/ |
|
public void setSelectedTextColor(Color c) { |
|
Color old = selectedTextColor; |
|
selectedTextColor = c; |
|
firePropertyChange("selectedTextColor", old, selectedTextColor); |
|
} |
|
/** |
|
* Fetches the current color used to render the |
|
* disabled text. |
|
* |
|
* @return the color |
|
*/ |
|
public Color getDisabledTextColor() { |
|
return disabledTextColor; |
|
} |
|
/** |
|
* Sets the current color used to render the |
|
* disabled text. Setting the color fires off a |
|
* PropertyChange event ("disabledTextColor"). |
|
* |
|
* @param c the color |
|
* @see #getDisabledTextColor |
|
* @beaninfo |
|
* description: color used to render disabled text |
|
* bound: true |
|
* preferred: true |
|
*/ |
|
public void setDisabledTextColor(Color c) { |
|
Color old = disabledTextColor; |
|
disabledTextColor = c; |
|
firePropertyChange("disabledTextColor", old, disabledTextColor); |
|
} |
|
/** |
|
* Replaces the currently selected content with new content |
|
* represented by the given string. If there is no selection |
|
* this amounts to an insert of the given text. If there |
|
* is no replacement text this amounts to a removal of the |
|
* current selection. |
|
* <p> |
|
* This is the method that is used by the default implementation |
|
* of the action for inserting content that gets bound to the |
|
* keymap actions. |
|
* |
|
* @param content the content to replace the selection with |
|
*/ |
|
public void replaceSelection(String content) { |
|
Document doc = getDocument(); |
|
if (doc != null) { |
|
try { |
|
boolean composedTextSaved = saveComposedText(caret.getDot()); |
|
int p0 = Math.min(caret.getDot(), caret.getMark()); |
|
int p1 = Math.max(caret.getDot(), caret.getMark()); |
|
if (doc instanceof AbstractDocument) { |
|
((AbstractDocument)doc).replace(p0, p1 - p0, content,null); |
|
} |
|
else { |
|
if (p0 != p1) { |
|
doc.remove(p0, p1 - p0); |
|
} |
|
if (content != null && content.length() > 0) { |
|
doc.insertString(p0, content, null); |
|
} |
|
} |
|
if (composedTextSaved) { |
|
restoreComposedText(); |
|
} |
|
} catch (BadLocationException e) { |
|
UIManager.getLookAndFeel().provideErrorFeedback(JTextComponent.this); |
|
} |
|
} |
|
} |
|
/** |
|
* Fetches a portion of the text represented by the |
|
* component. Returns an empty string if length is 0. |
|
* |
|
* @param offs the offset ≥ 0 |
|
* @param len the length ≥ 0 |
|
* @return the text |
|
* @exception BadLocationException if the offset or length are invalid |
|
*/ |
|
public String getText(int offs, int len) throws BadLocationException { |
|
return getDocument().getText(offs, len); |
|
} |
|
/** |
|
* Converts the given location in the model to a place in |
|
* the view coordinate system. |
|
* The component must have a positive size for |
|
* this translation to be computed (i.e. layout cannot |
|
* be computed until the component has been sized). The |
|
* component does not have to be visible or painted. |
|
* |
|
* @param pos the position ≥ 0 |
|
* @return the coordinates as a rectangle, with (r.x, r.y) as the location |
|
* in the coordinate system, or null if the component does |
|
* not yet have a positive size. |
|
* @exception BadLocationException if the given position does not |
|
* represent a valid location in the associated document |
|
* @see TextUI#modelToView |
|
*/ |
|
public Rectangle modelToView(int pos) throws BadLocationException { |
|
return getUI().modelToView(this, pos); |
|
} |
|
/** |
|
* Converts the given place in the view coordinate system |
|
* to the nearest representative location in the model. |
|
* The component must have a positive size for |
|
* this translation to be computed (i.e. layout cannot |
|
* be computed until the component has been sized). The |
|
* component does not have to be visible or painted. |
|
* |
|
* @param pt the location in the view to translate |
|
* @return the offset ≥ 0 from the start of the document, |
|
* or -1 if the component does not yet have a positive |
|
* size. |
|
* @see TextUI#viewToModel |
|
*/ |
|
public int viewToModel(Point pt) { |
|
return getUI().viewToModel(this, pt); |
|
} |
|
/** |
|
* Transfers the currently selected range in the associated |
|
* text model to the system clipboard, removing the contents |
|
* from the model. The current selection is reset. Does nothing |
|
* for <code>null</code> selections. |
|
* |
|
* @see java.awt.Toolkit#getSystemClipboard |
|
* @see java.awt.datatransfer.Clipboard |
|
*/ |
|
public void cut() { |
|
if (isEditable() && isEnabled()) { |
|
invokeAction("cut", TransferHandler.getCutAction()); |
|
} |
|
} |
|
/** |
|
* Transfers the currently selected range in the associated |
|
* text model to the system clipboard, leaving the contents |
|
* in the text model. The current selection remains intact. |
|
* Does nothing for <code>null</code> selections. |
|
* |
|
* @see java.awt.Toolkit#getSystemClipboard |
|
* @see java.awt.datatransfer.Clipboard |
|
*/ |
|
public void copy() { |
|
invokeAction("copy", TransferHandler.getCopyAction()); |
|
} |
|
/** |
|
* Transfers the contents of the system clipboard into the |
|
* associated text model. If there is a selection in the |
|
* associated view, it is replaced with the contents of the |
|
* clipboard. If there is no selection, the clipboard contents |
|
* are inserted in front of the current insert position in |
|
* the associated view. If the clipboard is empty, does nothing. |
|
* |
|
* @see #replaceSelection |
|
* @see java.awt.Toolkit#getSystemClipboard |
|
* @see java.awt.datatransfer.Clipboard |
|
*/ |
|
public void paste() { |
|
if (isEditable() && isEnabled()) { |
|
invokeAction("paste", TransferHandler.getPasteAction()); |
|
} |
|
} |
|
/** |
|
* This is a convenience method that is only useful for |
|
* <code>cut</code>, <code>copy</code> and <code>paste</code>. If |
|
* an <code>Action</code> with the name <code>name</code> does not |
|
* exist in the <code>ActionMap</code>, this will attempt to install a |
|
* <code>TransferHandler</code> and then use <code>altAction</code>. |
|
*/ |
|
private void invokeAction(String name, Action altAction) { |
|
ActionMap map = getActionMap(); |
|
Action action = null; |
|
if (map != null) { |
|
action = map.get(name); |
|
} |
|
if (action == null) { |
|
installDefaultTransferHandlerIfNecessary(); |
|
action = altAction; |
|
} |
|
action.actionPerformed(new ActionEvent(this, |
|
ActionEvent.ACTION_PERFORMED, (String)action. |
|
getValue(Action.NAME), |
|
EventQueue.getMostRecentEventTime(), |
|
getCurrentEventModifiers())); |
|
} |
|
/** |
|
* If the current <code>TransferHandler</code> is null, this will |
|
* install a new one. |
|
*/ |
|
private void installDefaultTransferHandlerIfNecessary() { |
|
if (getTransferHandler() == null) { |
|
if (defaultTransferHandler == null) { |
|
defaultTransferHandler = new DefaultTransferHandler(); |
|
} |
|
setTransferHandler(defaultTransferHandler); |
|
} |
|
} |
|
/** |
|
* Moves the caret to a new position, leaving behind a mark |
|
* defined by the last time <code>setCaretPosition</code> was |
|
* called. This forms a selection. |
|
* If the document is <code>null</code>, does nothing. The position |
|
* must be between 0 and the length of the component's text or else |
|
* an exception is thrown. |
|
* |
|
* @param pos the position |
|
* @exception IllegalArgumentException if the value supplied |
|
* for <code>position</code> is less than zero or greater |
|
* than the component's text length |
|
* @see #setCaretPosition |
|
*/ |
|
public void moveCaretPosition(int pos) { |
|
Document doc = getDocument(); |
|
if (doc != null) { |
|
if (pos > doc.getLength() || pos < 0) { |
|
throw new IllegalArgumentException("bad position: " + pos); |
|
} |
|
caret.moveDot(pos); |
|
} |
|
} |
|
/** |
|
* The bound property name for the focus accelerator. |
|
*/ |
|
public static final String FOCUS_ACCELERATOR_KEY = "focusAcceleratorKey"; |
|
/** |
|
* Sets the key accelerator that will cause the receiving text |
|
* component to get the focus. The accelerator will be the |
|
* key combination of the platform-specific modifier key and |
|
* the character given (converted to upper case). For example, |
|
* the ALT key is used as a modifier on Windows and the CTRL+ALT |
|
* combination is used on Mac. By default, there is no focus |
|
* accelerator key. Any previous key accelerator setting will be |
|
* superseded. A '\0' key setting will be registered, and has the |
|
* effect of turning off the focus accelerator. When the new key |
|
* is set, a PropertyChange event (FOCUS_ACCELERATOR_KEY) will be fired. |
|
* |
|
* @param aKey the key |
|
* @see #getFocusAccelerator |
|
* @beaninfo |
|
* description: accelerator character used to grab focus |
|
* bound: true |
|
*/ |
|
public void setFocusAccelerator(char aKey) { |
|
aKey = Character.toUpperCase(aKey); |
|
char old = focusAccelerator; |
|
focusAccelerator = aKey; |
|
// Fix for 4341002: value of FOCUS_ACCELERATOR_KEY is wrong. |
|
// So we fire both FOCUS_ACCELERATOR_KEY, for compatibility, |
|
// and the correct event here. |
|
firePropertyChange(FOCUS_ACCELERATOR_KEY, old, focusAccelerator); |
|
firePropertyChange("focusAccelerator", old, focusAccelerator); |
|
} |
|
/** |
|
* Returns the key accelerator that will cause the receiving |
|
* text component to get the focus. Return '\0' if no focus |
|
* accelerator has been set. |
|
* |
|
* @return the key |
|
*/ |
|
public char getFocusAccelerator() { |
|
return focusAccelerator; |
|
} |
|
/** |
|
* Initializes from a stream. This creates a |
|
* model of the type appropriate for the component |
|
* and initializes the model from the stream. |
|
* By default this will load the model as plain |
|
* text. Previous contents of the model are discarded. |
|
* |
|
* @param in the stream to read from |
|
* @param desc an object describing the stream; this |
|
* might be a string, a File, a URL, etc. Some kinds |
|
* of documents (such as html for example) might be |
|
* able to make use of this information; if non-<code>null</code>, |
|
* it is added as a property of the document |
|
* @exception IOException as thrown by the stream being |
|
* used to initialize |
|
* @see EditorKit#createDefaultDocument |
|
* @see #setDocument |
|
* @see PlainDocument |
|
*/ |
|
public void read(Reader in, Object desc) throws IOException { |
|
EditorKit kit = getUI().getEditorKit(this); |
|
Document doc = kit.createDefaultDocument(); |
|
if (desc != null) { |
|
doc.putProperty(Document.StreamDescriptionProperty, desc); |
|
} |
|
try { |
|
kit.read(in, doc, 0); |
|
setDocument(doc); |
|
} catch (BadLocationException e) { |
|
throw new IOException(e.getMessage()); |
|
} |
|
} |
|
/** |
|
* Stores the contents of the model into the given |
|
* stream. By default this will store the model as plain |
|
* text. |
|
* |
|
* @param out the output stream |
|
* @exception IOException on any I/O error |
|
*/ |
|
public void write(Writer out) throws IOException { |
|
Document doc = getDocument(); |
|
try { |
|
getUI().getEditorKit(this).write(out, doc, 0, doc.getLength()); |
|
} catch (BadLocationException e) { |
|
throw new IOException(e.getMessage()); |
|
} |
|
} |
|
public void removeNotify() { |
|
super.removeNotify(); |
|
if (getFocusedComponent() == this) { |
|
AppContext.getAppContext().remove(FOCUSED_COMPONENT); |
|
} |
|
} |
|
// --- java.awt.TextComponent methods ------------------------ |
|
/** |
|
* Sets the position of the text insertion caret for the |
|
* <code>TextComponent</code>. Note that the caret tracks change, |
|
* so this may move if the underlying text of the component is changed. |
|
* If the document is <code>null</code>, does nothing. The position |
|
* must be between 0 and the length of the component's text or else |
|
* an exception is thrown. |
|
* |
|
* @param position the position |
|
* @exception IllegalArgumentException if the value supplied |
|
* for <code>position</code> is less than zero or greater |
|
* than the component's text length |
|
* @beaninfo |
|
* description: the caret position |
|
*/ |
|
public void setCaretPosition(int position) { |
|
Document doc = getDocument(); |
|
if (doc != null) { |
|
if (position > doc.getLength() || position < 0) { |
|
throw new IllegalArgumentException("bad position: " + position); |
|
} |
|
caret.setDot(position); |
|
} |
|
} |
|
/** |
|
* Returns the position of the text insertion caret for the |
|
* text component. |
|
* |
|
* @return the position of the text insertion caret for the |
|
* text component ≥ 0 |
|
*/ |
|
@Transient |
|
public int getCaretPosition() { |
|
return caret.getDot(); |
|
} |
|
/** |
|
* Sets the text of this <code>TextComponent</code> |
|
* to the specified text. If the text is <code>null</code> |
|
* or empty, has the effect of simply deleting the old text. |
|
* When text has been inserted, the resulting caret location |
|
* is determined by the implementation of the caret class. |
|
* |
|
* <p> |
|
* Note that text is not a bound property, so no <code>PropertyChangeEvent |
|
* </code> is fired when it changes. To listen for changes to the text, |
|
* use <code>DocumentListener</code>. |
|
* |
|
* @param t the new text to be set |
|
* @see #getText |
|
* @see DefaultCaret |
|
* @beaninfo |
|
* description: the text of this component |
|
*/ |
|
public void setText(String t) { |
|
try { |
|
Document doc = getDocument(); |
|
if (doc instanceof AbstractDocument) { |
|
((AbstractDocument)doc).replace(0, doc.getLength(), t,null); |
|
} |
|
else { |
|
doc.remove(0, doc.getLength()); |
|
doc.insertString(0, t, null); |
|
} |
|
} catch (BadLocationException e) { |
|
UIManager.getLookAndFeel().provideErrorFeedback(JTextComponent.this); |
|
} |
|
} |
|
/** |
|
* Returns the text contained in this <code>TextComponent</code>. |
|
* If the underlying document is <code>null</code>, |
|
* will give a <code>NullPointerException</code>. |
|
* |
|
* Note that text is not a bound property, so no <code>PropertyChangeEvent |
|
* </code> is fired when it changes. To listen for changes to the text, |
|
* use <code>DocumentListener</code>. |
|
* |
|
* @return the text |
|
* @exception NullPointerException if the document is <code>null</code> |
|
* @see #setText |
|
*/ |
|
public String getText() { |
|
Document doc = getDocument(); |
|
String txt; |
|
try { |
|
txt = doc.getText(0, doc.getLength()); |
|
} catch (BadLocationException e) { |
|
txt = null; |
|
} |
|
return txt; |
|
} |
|
/** |
|
* Returns the selected text contained in this |
|
* <code>TextComponent</code>. If the selection is |
|
* <code>null</code> or the document empty, returns <code>null</code>. |
|
* |
|
* @return the text |
|
* @exception IllegalArgumentException if the selection doesn't |
|
* have a valid mapping into the document for some reason |
|
* @see #setText |
|
*/ |
|
public String getSelectedText() { |
|
String txt = null; |
|
int p0 = Math.min(caret.getDot(), caret.getMark()); |
|
int p1 = Math.max(caret.getDot(), caret.getMark()); |
|
if (p0 != p1) { |
|
try { |
|
Document doc = getDocument(); |
|
txt = doc.getText(p0, p1 - p0); |
|
} catch (BadLocationException e) { |
|
throw new IllegalArgumentException(e.getMessage()); |
|
} |
|
} |
|
return txt; |
|
} |
|
/** |
|
* Returns the boolean indicating whether this |
|
* <code>TextComponent</code> is editable or not. |
|
* |
|
* @return the boolean value |
|
* @see #setEditable |
|
*/ |
|
public boolean isEditable() { |
|
return editable; |
|
} |
|
/** |
|
* Sets the specified boolean to indicate whether or not this |
|
* <code>TextComponent</code> should be editable. |
|
* A PropertyChange event ("editable") is fired when the |
|
* state is changed. |
|
* |
|
* @param b the boolean to be set |
|
* @see #isEditable |
|
* @beaninfo |
|
* description: specifies if the text can be edited |
|
* bound: true |
|
*/ |
|
public void setEditable(boolean b) { |
|
if (b != editable) { |
|
boolean oldVal = editable; |
|
editable = b; |
|
enableInputMethods(editable); |
|
firePropertyChange("editable", Boolean.valueOf(oldVal), Boolean.valueOf(editable)); |
|
repaint(); |
|
} |
|
} |
|
/** |
|
* Returns the selected text's start position. Return 0 for an |
|
* empty document, or the value of dot if no selection. |
|
* |
|
* @return the start position ≥ 0 |
|
*/ |
|
@Transient |
|
public int getSelectionStart() { |
|
int start = Math.min(caret.getDot(), caret.getMark()); |
|
return start; |
|
} |
|
/** |
|
* Sets the selection start to the specified position. The new |
|
* starting point is constrained to be before or at the current |
|
* selection end. |
|
* <p> |
|
* This is available for backward compatibility to code |
|
* that called this method on <code>java.awt.TextComponent</code>. |
|
* This is implemented to forward to the <code>Caret</code> |
|
* implementation which is where the actual selection is maintained. |
|
* |
|
* @param selectionStart the start position of the text ≥ 0 |
|
* @beaninfo |
|
* description: starting location of the selection. |
|
*/ |
|
public void setSelectionStart(int selectionStart) { |
|
/* Route through select method to enforce consistent policy |
|
* between selectionStart and selectionEnd. |
|
*/ |
|
select(selectionStart, getSelectionEnd()); |
|
} |
|
/** |
|
* Returns the selected text's end position. Return 0 if the document |
|
* is empty, or the value of dot if there is no selection. |
|
* |
|
* @return the end position ≥ 0 |
|
*/ |
|
@Transient |
|
public int getSelectionEnd() { |
|
int end = Math.max(caret.getDot(), caret.getMark()); |
|
return end; |
|
} |
|
/** |
|
* Sets the selection end to the specified position. The new |
|
* end point is constrained to be at or after the current |
|
* selection start. |
|
* <p> |
|
* This is available for backward compatibility to code |
|
* that called this method on <code>java.awt.TextComponent</code>. |
|
* This is implemented to forward to the <code>Caret</code> |
|
* implementation which is where the actual selection is maintained. |
|
* |
|
* @param selectionEnd the end position of the text ≥ 0 |
|
* @beaninfo |
|
* description: ending location of the selection. |
|
*/ |
|
public void setSelectionEnd(int selectionEnd) { |
|
/* Route through select method to enforce consistent policy |
|
* between selectionStart and selectionEnd. |
|
*/ |
|
select(getSelectionStart(), selectionEnd); |
|
} |
|
/** |
|
* Selects the text between the specified start and end positions. |
|
* <p> |
|
* This method sets the start and end positions of the |
|
* selected text, enforcing the restriction that the start position |
|
* must be greater than or equal to zero. The end position must be |
|
* greater than or equal to the start position, and less than or |
|
* equal to the length of the text component's text. |
|
* <p> |
|
* If the caller supplies values that are inconsistent or out of |
|
* bounds, the method enforces these constraints silently, and |
|
* without failure. Specifically, if the start position or end |
|
* position is greater than the length of the text, it is reset to |
|
* equal the text length. If the start position is less than zero, |
|
* it is reset to zero, and if the end position is less than the |
|
* start position, it is reset to the start position. |
|
* <p> |
|
* This call is provided for backward compatibility. |
|
* It is routed to a call to <code>setCaretPosition</code> |
|
* followed by a call to <code>moveCaretPosition</code>. |
|
* The preferred way to manage selection is by calling |
|
* those methods directly. |
|
* |
|
* @param selectionStart the start position of the text |
|
* @param selectionEnd the end position of the text |
|
* @see #setCaretPosition |
|
* @see #moveCaretPosition |
|
*/ |
|
public void select(int selectionStart, int selectionEnd) { |
|
// argument adjustment done by java.awt.TextComponent |
|
int docLength = getDocument().getLength(); |
|
if (selectionStart < 0) { |
|
selectionStart = 0; |
|
} |
|
if (selectionStart > docLength) { |
|
selectionStart = docLength; |
|
} |
|
if (selectionEnd > docLength) { |
|
selectionEnd = docLength; |
|
} |
|
if (selectionEnd < selectionStart) { |
|
selectionEnd = selectionStart; |
|
} |
|
setCaretPosition(selectionStart); |
|
moveCaretPosition(selectionEnd); |
|
} |
|
/** |
|
* Selects all the text in the <code>TextComponent</code>. |
|
* Does nothing on a <code>null</code> or empty document. |
|
*/ |
|
public void selectAll() { |
|
Document doc = getDocument(); |
|
if (doc != null) { |
|
setCaretPosition(0); |
|
moveCaretPosition(doc.getLength()); |
|
} |
|
} |
|
// --- Tooltip Methods --------------------------------------------- |
|
/** |
|
* Returns the string to be used as the tooltip for <code>event</code>. |
|
* This will return one of: |
|
* <ol> |
|
* <li>If <code>setToolTipText</code> has been invoked with a |
|
* non-<code>null</code> |
|
* value, it will be returned, otherwise |
|
* <li>The value from invoking <code>getToolTipText</code> on |
|
* the UI will be returned. |
|
* </ol> |
|
* By default <code>JTextComponent</code> does not register |
|
* itself with the <code>ToolTipManager</code>. |
|
* This means that tooltips will NOT be shown from the |
|
* <code>TextUI</code> unless <code>registerComponent</code> has |
|
* been invoked on the <code>ToolTipManager</code>. |
|
* |
|
* @param event the event in question |
|
* @return the string to be used as the tooltip for <code>event</code> |
|
* @see javax.swing.JComponent#setToolTipText |
|
* @see javax.swing.plaf.TextUI#getToolTipText |
|
* @see javax.swing.ToolTipManager#registerComponent |
|
*/ |
|
public String getToolTipText(MouseEvent event) { |
|
String retValue = super.getToolTipText(event); |
|
if (retValue == null) { |
|
TextUI ui = getUI(); |
|
if (ui != null) { |
|
retValue = ui.getToolTipText(this, new Point(event.getX(), |
|
event.getY())); |
|
} |
|
} |
|
return retValue; |
|
} |
|
// --- Scrollable methods --------------------------------------------- |
|
/** |
|
* Returns the preferred size of the viewport for a view component. |
|
* This is implemented to do the default behavior of returning |
|
* the preferred size of the component. |
|
* |
|
* @return the <code>preferredSize</code> of a <code>JViewport</code> |
|
* whose view is this <code>Scrollable</code> |
|
*/ |
|
public Dimension getPreferredScrollableViewportSize() { |
|
return getPreferredSize(); |
|
} |
|
/** |
|
* Components that display logical rows or columns should compute |
|
* the scroll increment that will completely expose one new row |
|
* or column, depending on the value of orientation. Ideally, |
|
* components should handle a partially exposed row or column by |
|
* returning the distance required to completely expose the item. |
|
* <p> |
|
* The default implementation of this is to simply return 10% of |
|
* the visible area. Subclasses are likely to be able to provide |
|
* a much more reasonable value. |
|
* |
|
* @param visibleRect the view area visible within the viewport |
|
* @param orientation either <code>SwingConstants.VERTICAL</code> or |
|
* <code>SwingConstants.HORIZONTAL</code> |
|
* @param direction less than zero to scroll up/left, greater than |
|
* zero for down/right |
|
* @return the "unit" increment for scrolling in the specified direction |
|
* @exception IllegalArgumentException for an invalid orientation |
|
* @see JScrollBar#setUnitIncrement |
|
*/ |
|
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { |
|
switch(orientation) { |
|
case SwingConstants.VERTICAL: |
|
return visibleRect.height / 10; |
|
case SwingConstants.HORIZONTAL: |
|
return visibleRect.width / 10; |
|
default: |
|
throw new IllegalArgumentException("Invalid orientation: " + orientation); |
|
} |
|
} |
|
/** |
|
* Components that display logical rows or columns should compute |
|
* the scroll increment that will completely expose one block |
|
* of rows or columns, depending on the value of orientation. |
|
* <p> |
|
* The default implementation of this is to simply return the visible |
|
* area. Subclasses will likely be able to provide a much more |
|
* reasonable value. |
|
* |
|
* @param visibleRect the view area visible within the viewport |
|
* @param orientation either <code>SwingConstants.VERTICAL</code> or |
|
* <code>SwingConstants.HORIZONTAL</code> |
|
* @param direction less than zero to scroll up/left, greater than zero |
|
* for down/right |
|
* @return the "block" increment for scrolling in the specified direction |
|
* @exception IllegalArgumentException for an invalid orientation |
|
* @see JScrollBar#setBlockIncrement |
|
*/ |
|
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { |
|
switch(orientation) { |
|
case SwingConstants.VERTICAL: |
|
return visibleRect.height; |
|
case SwingConstants.HORIZONTAL: |
|
return visibleRect.width; |
|
default: |
|
throw new IllegalArgumentException("Invalid orientation: " + orientation); |
|
} |
|
} |
|
/** |
|
* Returns true if a viewport should always force the width of this |
|
* <code>Scrollable</code> to match the width of the viewport. |
|
* For example a normal text view that supported line wrapping |
|
* would return true here, since it would be undesirable for |
|
* wrapped lines to disappear beyond the right |
|
* edge of the viewport. Note that returning true for a |
|
* <code>Scrollable</code> whose ancestor is a <code>JScrollPane</code> |
|
* effectively disables horizontal scrolling. |
|
* <p> |
|
* Scrolling containers, like <code>JViewport</code>, |
|
* will use this method each time they are validated. |
|
* |
|
* @return true if a viewport should force the <code>Scrollable</code>s |
|
* width to match its own |
|
*/ |
|
public boolean getScrollableTracksViewportWidth() { |
|
Container parent = SwingUtilities.getUnwrappedParent(this); |
|
if (parent instanceof JViewport) { |
|
return parent.getWidth() > getPreferredSize().width; |
|
} |
|
return false; |
|
} |
|
/** |
|
* Returns true if a viewport should always force the height of this |
|
* <code>Scrollable</code> to match the height of the viewport. |
|
* For example a columnar text view that flowed text in left to |
|
* right columns could effectively disable vertical scrolling by |
|
* returning true here. |
|
* <p> |
|
* Scrolling containers, like <code>JViewport</code>, |
|
* will use this method each time they are validated. |
|
* |
|
* @return true if a viewport should force the Scrollables height |
|
* to match its own |
|
*/ |
|
public boolean getScrollableTracksViewportHeight() { |
|
Container parent = SwingUtilities.getUnwrappedParent(this); |
|
if (parent instanceof JViewport) { |
|
return parent.getHeight() > getPreferredSize().height; |
|
} |
|
return false; |
|
} |
|
////////////////// |
|
// Printing Support |
|
////////////////// |
|
/** |
|
* A convenience print method that displays a print dialog, and then |
|
* prints this {@code JTextComponent} in <i>interactive</i> mode with no |
|
* header or footer text. Note: this method |
|
* blocks until printing is done. |
|
* <p> |
|
* Note: In <i>headless</i> mode, no dialogs will be shown. |
|
* |
|
* <p> This method calls the full featured |
|
* {@link #print(MessageFormat, MessageFormat, boolean, PrintService, PrintRequestAttributeSet, boolean) |
|
* print} method to perform printing. |
|
* @return {@code true}, unless printing is canceled by the user |
|
* @throws PrinterException if an error in the print system causes the job |
|
* to be aborted |
|
* @throws SecurityException if this thread is not allowed to |
|
* initiate a print job request |
|
* |
|
* @see #print(MessageFormat, MessageFormat, boolean, PrintService, PrintRequestAttributeSet, boolean) |
|
* |
|
* @since 1.6 |
|
*/ |
|
public boolean print() throws PrinterException { |
|
return print(null, null, true, null, null, true); |
|
} |
|
/** |
|
* A convenience print method that displays a print dialog, and then |
|
* prints this {@code JTextComponent} in <i>interactive</i> mode with |
|
* the specified header and footer text. Note: this method |
|
* blocks until printing is done. |
|
* <p> |
|
* Note: In <i>headless</i> mode, no dialogs will be shown. |
|
* |
|
* <p> This method calls the full featured |
|
* {@link #print(MessageFormat, MessageFormat, boolean, PrintService, PrintRequestAttributeSet, boolean) |
|
* print} method to perform printing. |
|
* @param headerFormat the text, in {@code MessageFormat}, to be |
|
* used as the header, or {@code null} for no header |
|
* @param footerFormat the text, in {@code MessageFormat}, to be |
|
* used as the footer, or {@code null} for no footer |
|
* @return {@code true}, unless printing is canceled by the user |
|
* @throws PrinterException if an error in the print system causes the job |
|
* to be aborted |
|
* @throws SecurityException if this thread is not allowed to |
|
* initiate a print job request |
|
* |
|
* @see #print(MessageFormat, MessageFormat, boolean, PrintService, PrintRequestAttributeSet, boolean) |
|
* @see java.text.MessageFormat |
|
* @since 1.6 |
|
*/ |
|
public boolean print(final MessageFormat headerFormat, |
|
final MessageFormat footerFormat) throws PrinterException { |
|
return print(headerFormat, footerFormat, true, null, null, true); |
|
} |
|
/** |
|
* Prints the content of this {@code JTextComponent}. Note: this method |
|
* blocks until printing is done. |
|
* |
|
* <p> |
|
* Page header and footer text can be added to the output by providing |
|
* {@code MessageFormat} arguments. The printing code requests |
|
* {@code Strings} from the formats, providing a single item which may be |
|
* included in the formatted string: an {@code Integer} representing the |
|
* current page number. |
|
* |
|
* <p> |
|
* {@code showPrintDialog boolean} parameter allows you to specify whether |
|
* a print dialog is displayed to the user. When it is, the user |
|
* may use the dialog to change printing attributes or even cancel the |
|
* print. |
|
* |
|
* <p> |
|
* {@code service} allows you to provide the initial |
|
* {@code PrintService} for the print dialog, or to specify |
|
* {@code PrintService} to print to when the dialog is not shown. |
|
* |
|
* <p> |
|
* {@code attributes} can be used to provide the |
|
* initial values for the print dialog, or to supply any needed |
|
* attributes when the dialog is not shown. {@code attributes} can |
|
* be used to control how the job will print, for example |
|
* <i>duplex</i> or <i>single-sided</i>. |
|
* |
|
* <p> |
|
* {@code interactive boolean} parameter allows you to specify |
|
* whether to perform printing in <i>interactive</i> |
|
* mode. If {@code true}, a progress dialog, with an abort option, |
|
* is displayed for the duration of printing. This dialog is |
|
* <i>modal</i> when {@code print} is invoked on the <i>Event Dispatch |
|
* Thread</i> and <i>non-modal</i> otherwise. <b>Warning</b>: |
|
* calling this method on the <i>Event Dispatch Thread</i> with {@code |
|
* interactive false} blocks <i>all</i> events, including repaints, from |
|
* being processed until printing is complete. It is only |
|
* recommended when printing from an application with no |
|
* visible GUI. |
|
* |
|
* <p> |
|
* Note: In <i>headless</i> mode, {@code showPrintDialog} and |
|
* {@code interactive} parameters are ignored and no dialogs are |
|
* shown. |
|
* |
|
* <p> |
|
* This method ensures the {@code document} is not mutated during printing. |
|
* To indicate it visually, {@code setEnabled(false)} is set for the |
|
* duration of printing. |
|
* |
|
* <p> |
|
* This method uses {@link #getPrintable} to render document content. |
|
* |
|
* <p> |
|
* This method is thread-safe, although most Swing methods are not. Please |
|
* see <A |
|
* HREF="https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html"> |
|
* Concurrency in Swing</A> for more information. |
|
* |
|
* <p> |
|
* <b>Sample Usage</b>. This code snippet shows a cross-platform print |
|
* dialog and then prints the {@code JTextComponent} in <i>interactive</i> mode |
|
* unless the user cancels the dialog: |
|
* |
|
* <pre> |
|
* textComponent.print(new MessageFormat("My text component header"), |
|
* new MessageFormat("Footer. Page - {0}"), true, null, null, true); |
|
* </pre> |
|
* <p> |
|
* Executing this code off the <i>Event Dispatch Thread</i> |
|
* performs printing on the <i>background</i>. |
|
* The following pattern might be used for <i>background</i> |
|
* printing: |
|
* <pre> |
|
* FutureTask<Boolean> future = |
|
* new FutureTask<Boolean>( |
|
* new Callable<Boolean>() { |
|
* public Boolean call() { |
|
* return textComponent.print(.....); |
|
* } |
|
* }); |
|
* executor.execute(future); |
|
* </pre> |
|
* |
|
* @param headerFormat the text, in {@code MessageFormat}, to be |
|
* used as the header, or {@code null} for no header |
|
* @param footerFormat the text, in {@code MessageFormat}, to be |
|
* used as the footer, or {@code null} for no footer |
|
* @param showPrintDialog {@code true} to display a print dialog, |
|
* {@code false} otherwise |
|
* @param service initial {@code PrintService}, or {@code null} for the |
|
* default |
|
* @param attributes the job attributes to be applied to the print job, or |
|
* {@code null} for none |
|
* @param interactive whether to print in an interactive mode |
|
* @return {@code true}, unless printing is canceled by the user |
|
* @throws PrinterException if an error in the print system causes the job |
|
* to be aborted |
|
* @throws SecurityException if this thread is not allowed to |
|
* initiate a print job request |
|
* |
|
* @see #getPrintable |
|
* @see java.text.MessageFormat |
|
* @see java.awt.GraphicsEnvironment#isHeadless |
|
* @see java.util.concurrent.FutureTask |
|
* |
|
* @since 1.6 |
|
*/ |
|
public boolean print(final MessageFormat headerFormat, |
|
final MessageFormat footerFormat, |
|
final boolean showPrintDialog, |
|
final PrintService service, |
|
final PrintRequestAttributeSet attributes, |
|
final boolean interactive) |
|
throws PrinterException { |
|
final PrinterJob job = PrinterJob.getPrinterJob(); |
|
final Printable printable; |
|
final PrintingStatus printingStatus; |
|
final boolean isHeadless = GraphicsEnvironment.isHeadless(); |
|
final boolean isEventDispatchThread = |
|
SwingUtilities.isEventDispatchThread(); |
|
final Printable textPrintable = getPrintable(headerFormat, footerFormat); |
|
if (interactive && ! isHeadless) { |
|
printingStatus = |
|
PrintingStatus.createPrintingStatus(this, job); |
|
printable = |
|
printingStatus.createNotificationPrintable(textPrintable); |
|
} else { |
|
printingStatus = null; |
|
printable = textPrintable; |
|
} |
|
if (service != null) { |
|
job.setPrintService(service); |
|
} |
|
job.setPrintable(printable); |
|
final PrintRequestAttributeSet attr = (attributes == null) |
|
? new HashPrintRequestAttributeSet() |
|
: attributes; |
|
if (showPrintDialog && ! isHeadless && ! job.printDialog(attr)) { |
|
return false; |
|
} |
|
/* |
|
* there are three cases for printing: |
|
* 1. print non interactively (! interactive || isHeadless) |
|
* 2. print interactively off EDT |
|
* 3. print interactively on EDT |
|
* |
|
* 1 and 2 prints on the current thread (3 prints on another thread) |
|
* 2 and 3 deal with PrintingStatusDialog |
|
*/ |
|
final Callable<Object> doPrint = |
|
new Callable<Object>() { |
|
public Object call() throws Exception { |
|
try { |
|
job.print(attr); |
|
} finally { |
|
if (printingStatus != null) { |
|
printingStatus.dispose(); |
|
} |
|
} |
|
return null; |
|
} |
|
}; |
|
final FutureTask<Object> futurePrinting = |
|
new FutureTask<Object>(doPrint); |
|
final Runnable runnablePrinting = |
|
new Runnable() { |
|
public void run() { |
|
//disable component |
|
boolean wasEnabled = false; |
|
if (isEventDispatchThread) { |
|
if (isEnabled()) { |
|
wasEnabled = true; |
|
setEnabled(false); |
|
} |
|
} else { |
|
try { |
|
wasEnabled = SwingUtilities2.submit( |
|
new Callable<Boolean>() { |
|
public Boolean call() throws Exception { |
|
boolean rv = isEnabled(); |
|
if (rv) { |
|
setEnabled(false); |
|
} |
|
return rv; |
|
} |
|
}).get(); |
|
} catch (InterruptedException e) { |
|
throw new RuntimeException(e); |
|
} catch (ExecutionException e) { |
|
Throwable cause = e.getCause(); |
|
if (cause instanceof Error) { |
|
throw (Error) cause; |
|
} |
|
if (cause instanceof RuntimeException) { |
|
throw (RuntimeException) cause; |
|
} |
|
throw new AssertionError(cause); |
|
} |
|
} |
|
getDocument().render(futurePrinting); |
|
//enable component |
|
if (wasEnabled) { |
|
if (isEventDispatchThread) { |
|
setEnabled(true); |
|
} else { |
|
try { |
|
SwingUtilities2.submit( |
|
new Runnable() { |
|
public void run() { |
|
setEnabled(true); |
|
} |
|
}, null).get(); |
|
} catch (InterruptedException e) { |
|
throw new RuntimeException(e); |
|
} catch (ExecutionException e) { |
|
Throwable cause = e.getCause(); |
|
if (cause instanceof Error) { |
|
throw (Error) cause; |
|
} |
|
if (cause instanceof RuntimeException) { |
|
throw (RuntimeException) cause; |
|
} |
|
throw new AssertionError(cause); |
|
} |
|
} |
|
} |
|
} |
|
}; |
|
if (! interactive || isHeadless) { |
|
runnablePrinting.run(); |
|
} else { |
|
if (isEventDispatchThread) { |
|
(new Thread(runnablePrinting)).start(); |
|
printingStatus.showModal(true); |
|
} else { |
|
printingStatus.showModal(false); |
|
runnablePrinting.run(); |
|
} |
|
} |
|
//the printing is done successfully or otherwise. |
|
//dialog is hidden if needed. |
|
try { |
|
futurePrinting.get(); |
|
} catch (InterruptedException e) { |
|
throw new RuntimeException(e); |
|
} catch (ExecutionException e) { |
|
Throwable cause = e.getCause(); |
|
if (cause instanceof PrinterAbortException) { |
|
if (printingStatus != null |
|
&& printingStatus.isAborted()) { |
|
return false; |
|
} else { |
|
throw (PrinterAbortException) cause; |
|
} |
|
} else if (cause instanceof PrinterException) { |
|
throw (PrinterException) cause; |
|
} else if (cause instanceof RuntimeException) { |
|
throw (RuntimeException) cause; |
|
} else if (cause instanceof Error) { |
|
throw (Error) cause; |
|
} else { |
|
throw new AssertionError(cause); |
|
} |
|
} |
|
return true; |
|
} |
|
/** |
|
* Returns a {@code Printable} to use for printing the content of this |
|
* {@code JTextComponent}. The returned {@code Printable} prints |
|
* the document as it looks on the screen except being reformatted |
|
* to fit the paper. |
|
* The returned {@code Printable} can be wrapped inside another |
|
* {@code Printable} in order to create complex reports and |
|
* documents. |
|
* |
|
* |
|
* <p> |
|
* The returned {@code Printable} shares the {@code document} with this |
|
* {@code JTextComponent}. It is the responsibility of the developer to |
|
* ensure that the {@code document} is not mutated while this {@code Printable} |
|
* is used. Printing behavior is undefined when the {@code document} is |
|
* mutated during printing. |
|
* |
|
* <p> |
|
* Page header and footer text can be added to the output by providing |
|
* {@code MessageFormat} arguments. The printing code requests |
|
* {@code Strings} from the formats, providing a single item which may be |
|
* included in the formatted string: an {@code Integer} representing the |
|
* current page number. |
|
* |
|
* <p> |
|
* The returned {@code Printable} when printed, formats the |
|
* document content appropriately for the page size. For correct |
|
* line wrapping the {@code imageable width} of all pages must be the |
|
* same. See {@link java.awt.print.PageFormat#getImageableWidth}. |
|
* |
|
* <p> |
|
* This method is thread-safe, although most Swing methods are not. Please |
|
* see <A |
|
* HREF="https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html"> |
|
* Concurrency in Swing</A> for more information. |
|
* |
|
* <p> |
|
* The returned {@code Printable} can be printed on any thread. |
|
* |
|
* <p> |
|
* This implementation returned {@code Printable} performs all painting on |
|
* the <i>Event Dispatch Thread</i>, regardless of what thread it is |
|
* used on. |
|
* |
|
* @param headerFormat the text, in {@code MessageFormat}, to be |
|
* used as the header, or {@code null} for no header |
|
* @param footerFormat the text, in {@code MessageFormat}, to be |
|
* used as the footer, or {@code null} for no footer |
|
* @return a {@code Printable} for use in printing content of this |
|
* {@code JTextComponent} |
|
* |
|
* |
|
* @see java.awt.print.Printable |
|
* @see java.awt.print.PageFormat |
|
* @see javax.swing.text.Document#render(java.lang.Runnable) |
|
* |
|
* @since 1.6 |
|
*/ |
|
public Printable getPrintable(final MessageFormat headerFormat, |
|
final MessageFormat footerFormat) { |
|
return TextComponentPrintable.getPrintable( |
|
this, headerFormat, footerFormat); |
|
} |
|
///////////////// |
|
// Accessibility support |
|
//////////////// |
|
/** |
|
* Gets the <code>AccessibleContext</code> associated with this |
|
* <code>JTextComponent</code>. For text components, |
|
* the <code>AccessibleContext</code> takes the form of an |
|
* <code>AccessibleJTextComponent</code>. |
|
* A new <code>AccessibleJTextComponent</code> instance |
|
* is created if necessary. |
|
* |
|
* @return an <code>AccessibleJTextComponent</code> that serves as the |
|
* <code>AccessibleContext</code> of this |
|
* <code>JTextComponent</code> |
|
*/ |
|
public AccessibleContext getAccessibleContext() { |
|
if (accessibleContext == null) { |
|
accessibleContext = new AccessibleJTextComponent(); |
|
} |
|
return accessibleContext; |
|
} |
|
/** |
|
* This class implements accessibility support for the |
|
* <code>JTextComponent</code> class. It provides an implementation of |
|
* the Java Accessibility API appropriate to menu user-interface elements. |
|
* <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™ |
|
* has been added to the <code>java.beans</code> package. |
|
* Please see {@link java.beans.XMLEncoder}. |
|
*/ |
|
public class AccessibleJTextComponent extends AccessibleJComponent |
|
implements AccessibleText, CaretListener, DocumentListener, |
|
AccessibleAction, AccessibleEditableText, |
|
AccessibleExtendedText { |
|
int caretPos; |
|
Point oldLocationOnScreen; |
|
/** |
|
* Constructs an AccessibleJTextComponent. Adds a listener to track |
|
* caret change. |
|
*/ |
|
public AccessibleJTextComponent() { |
|
Document doc = JTextComponent.this.getDocument(); |
|
if (doc != null) { |
|
doc.addDocumentListener(this); |
|
} |
|
JTextComponent.this.addCaretListener(this); |
|
caretPos = getCaretPosition(); |
|
try { |
|
oldLocationOnScreen = getLocationOnScreen(); |
|
} catch (IllegalComponentStateException iae) { |
|
} |
|
// Fire a ACCESSIBLE_VISIBLE_DATA_PROPERTY PropertyChangeEvent |
|
// when the text component moves (e.g., when scrolling). |
|
// Using an anonymous class since making AccessibleJTextComponent |
|
// implement ComponentListener would be an API change. |
|
JTextComponent.this.addComponentListener(new ComponentAdapter() { |
|
public void componentMoved(ComponentEvent e) { |
|
try { |
|
Point newLocationOnScreen = getLocationOnScreen(); |
|
firePropertyChange(ACCESSIBLE_VISIBLE_DATA_PROPERTY, |
|
oldLocationOnScreen, |
|
newLocationOnScreen); |
|
oldLocationOnScreen = newLocationOnScreen; |
|
} catch (IllegalComponentStateException iae) { |
|
} |
|
} |
|
}); |
|
} |
|
/** |
|
* Handles caret updates (fire appropriate property change event, |
|
* which are AccessibleContext.ACCESSIBLE_CARET_PROPERTY and |
|
* AccessibleContext.ACCESSIBLE_SELECTION_PROPERTY). |
|
* This keeps track of the dot position internally. When the caret |
|
* moves, the internal position is updated after firing the event. |
|
* |
|
* @param e the CaretEvent |
|
*/ |
|
public void caretUpdate(CaretEvent e) { |
|
int dot = e.getDot(); |
|
int mark = e.getMark(); |
|
if (caretPos != dot) { |
|
// the caret moved |
|
firePropertyChange(ACCESSIBLE_CARET_PROPERTY, |
|
new Integer(caretPos), new Integer(dot)); |
|
caretPos = dot; |
|
try { |
|
oldLocationOnScreen = getLocationOnScreen(); |
|
} catch (IllegalComponentStateException iae) { |
|
} |
|
} |
|
if (mark != dot) { |
|
// there is a selection |
|
firePropertyChange(ACCESSIBLE_SELECTION_PROPERTY, null, |
|
getSelectedText()); |
|
} |
|
} |
|
// DocumentListener methods |
|
/** |
|
* Handles document insert (fire appropriate property change event |
|
* which is AccessibleContext.ACCESSIBLE_TEXT_PROPERTY). |
|
* This tracks the changed offset via the event. |
|
* |
|
* @param e the DocumentEvent |
|
*/ |
|
public void insertUpdate(DocumentEvent e) { |
|
final Integer pos = new Integer (e.getOffset()); |
|
if (SwingUtilities.isEventDispatchThread()) { |
|
firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, pos); |
|
} else { |
|
Runnable doFire = new Runnable() { |
|
public void run() { |
|
firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, |
|
null, pos); |
|
} |
|
}; |
|
SwingUtilities.invokeLater(doFire); |
|
} |
|
} |
|
/** |
|
* Handles document remove (fire appropriate property change event, |
|
* which is AccessibleContext.ACCESSIBLE_TEXT_PROPERTY). |
|
* This tracks the changed offset via the event. |
|
* |
|
* @param e the DocumentEvent |
|
*/ |
|
public void removeUpdate(DocumentEvent e) { |
|
final Integer pos = new Integer (e.getOffset()); |
|
if (SwingUtilities.isEventDispatchThread()) { |
|
firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, pos); |
|
} else { |
|
Runnable doFire = new Runnable() { |
|
public void run() { |
|
firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, |
|
null, pos); |
|
} |
|
}; |
|
SwingUtilities.invokeLater(doFire); |
|
} |
|
} |
|
/** |
|
* Handles document remove (fire appropriate property change event, |
|
* which is AccessibleContext.ACCESSIBLE_TEXT_PROPERTY). |
|
* This tracks the changed offset via the event. |
|
* |
|
* @param e the DocumentEvent |
|
*/ |
|
public void changedUpdate(DocumentEvent e) { |
|
final Integer pos = new Integer (e.getOffset()); |
|
if (SwingUtilities.isEventDispatchThread()) { |
|
firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, pos); |
|
} else { |
|
Runnable doFire = new Runnable() { |
|
public void run() { |
|
firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, |
|
null, pos); |
|
} |
|
}; |
|
SwingUtilities.invokeLater(doFire); |
|
} |
|
} |
|
/** |
|
* Gets the state set of the JTextComponent. |
|
* The AccessibleStateSet of an object is composed of a set of |
|
* unique AccessibleState's. A change in the AccessibleStateSet |
|
* of an object will cause a PropertyChangeEvent to be fired |
|
* for the AccessibleContext.ACCESSIBLE_STATE_PROPERTY property. |
|
* |
|
* @return an instance of AccessibleStateSet containing the |
|
* current state set of the object |
|
* @see AccessibleStateSet |
|
* @see AccessibleState |
|
* @see #addPropertyChangeListener |
|
*/ |
|
public AccessibleStateSet getAccessibleStateSet() { |
|
AccessibleStateSet states = super.getAccessibleStateSet(); |
|
if (JTextComponent.this.isEditable()) { |
|
states.add(AccessibleState.EDITABLE); |
|
} |
|
return states; |
|
} |
|
/** |
|
* Gets the role of this object. |
|
* |
|
* @return an instance of AccessibleRole describing the role of the |
|
* object (AccessibleRole.TEXT) |
|
* @see AccessibleRole |
|
*/ |
|
public AccessibleRole getAccessibleRole() { |
|
return AccessibleRole.TEXT; |
|
} |
|
/** |
|
* Get the AccessibleText associated with this object. In the |
|
* implementation of the Java Accessibility API for this class, |
|
* return this object, which is responsible for implementing the |
|
* AccessibleText interface on behalf of itself. |
|
* |
|
* @return this object |
|
*/ |
|
public AccessibleText getAccessibleText() { |
|
return this; |
|
} |
|
// --- interface AccessibleText methods ------------------------ |
|
/** |
|
* Many of these methods are just convenience methods; they |
|
* just call the equivalent on the parent |
|
*/ |
|
/** |
|
* Given a point in local coordinates, return the zero-based index |
|
* of the character under that Point. If the point is invalid, |
|
* this method returns -1. |
|
* |
|
* @param p the Point in local coordinates |
|
* @return the zero-based index of the character under Point p. |
|
*/ |
|
public int getIndexAtPoint(Point p) { |
|
if (p == null) { |
|
return -1; |
|
} |
|
return JTextComponent.this.viewToModel(p); |
|
} |
|
/** |
|
* Gets the editor's drawing rectangle. Stolen |
|
* from the unfortunately named |
|
* BasicTextUI.getVisibleEditorRect() |
|
* |
|
* @return the bounding box for the root view |
|
*/ |
|
Rectangle getRootEditorRect() { |
|
Rectangle alloc = JTextComponent.this.getBounds(); |
|
if ((alloc.width > 0) && (alloc.height > 0)) { |
|
alloc.x = alloc.y = 0; |
|
Insets insets = JTextComponent.this.getInsets(); |
|
alloc.x += insets.left; |
|
alloc.y += insets.top; |
|
alloc.width -= insets.left + insets.right; |
|
alloc.height -= insets.top + insets.bottom; |
|
return alloc; |
|
} |
|
return null; |
|
} |
|
/** |
|
* Determines the bounding box of the character at the given |
|
* index into the string. The bounds are returned in local |
|
* coordinates. If the index is invalid a null rectangle |
|
* is returned. |
|
* |
|
* The screen coordinates returned are "unscrolled coordinates" |
|
* if the JTextComponent is contained in a JScrollPane in which |
|
* case the resulting rectangle should be composed with the parent |
|
* coordinates. A good algorithm to use is: |
|
* <pre> |
|
* Accessible a: |
|
* AccessibleText at = a.getAccessibleText(); |
|
* AccessibleComponent ac = a.getAccessibleComponent(); |
|
* Rectangle r = at.getCharacterBounds(); |
|
* Point p = ac.getLocation(); |
|
* r.x += p.x; |
|
* r.y += p.y; |
|
* </pre> |
|
* |
|
* Note: the JTextComponent must have a valid size (e.g. have |
|
* been added to a parent container whose ancestor container |
|
* is a valid top-level window) for this method to be able |
|
* to return a meaningful (non-null) value. |
|
* |
|
* @param i the index into the String ≥ 0 |
|
* @return the screen coordinates of the character's bounding box |
|
*/ |
|
public Rectangle getCharacterBounds(int i) { |
|
if (i < 0 || i > model.getLength()-1) { |
|
return null; |
|
} |
|
TextUI ui = getUI(); |
|
if (ui == null) { |
|
return null; |
|
} |
|
Rectangle rect = null; |
|
Rectangle alloc = getRootEditorRect(); |
|
if (alloc == null) { |
|
return null; |
|
} |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readLock(); |
|
} |
|
try { |
|
View rootView = ui.getRootView(JTextComponent.this); |
|
if (rootView != null) { |
|
rootView.setSize(alloc.width, alloc.height); |
|
Shape bounds = rootView.modelToView(i, |
|
Position.Bias.Forward, i+1, |
|
Position.Bias.Backward, alloc); |
|
rect = (bounds instanceof Rectangle) ? |
|
(Rectangle)bounds : bounds.getBounds(); |
|
} |
|
} catch (BadLocationException e) { |
|
} finally { |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readUnlock(); |
|
} |
|
} |
|
return rect; |
|
} |
|
/** |
|
* Returns the number of characters (valid indices) |
|
* |
|
* @return the number of characters ≥ 0 |
|
*/ |
|
public int getCharCount() { |
|
return model.getLength(); |
|
} |
|
/** |
|
* Returns the zero-based offset of the caret. |
|
* |
|
* Note: The character to the right of the caret will have the |
|
* same index value as the offset (the caret is between |
|
* two characters). |
|
* |
|
* @return the zero-based offset of the caret. |
|
*/ |
|
public int getCaretPosition() { |
|
return JTextComponent.this.getCaretPosition(); |
|
} |
|
/** |
|
* Returns the AttributeSet for a given character (at a given index). |
|
* |
|
* @param i the zero-based index into the text |
|
* @return the AttributeSet of the character |
|
*/ |
|
public AttributeSet getCharacterAttribute(int i) { |
|
Element e = null; |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readLock(); |
|
} |
|
try { |
|
for (e = model.getDefaultRootElement(); ! e.isLeaf(); ) { |
|
int index = e.getElementIndex(i); |
|
e = e.getElement(index); |
|
} |
|
} finally { |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readUnlock(); |
|
} |
|
} |
|
return e.getAttributes(); |
|
} |
|
/** |
|
* Returns the start offset within the selected text. |
|
* If there is no selection, but there is |
|
* a caret, the start and end offsets will be the same. |
|
* Return 0 if the text is empty, or the caret position |
|
* if no selection. |
|
* |
|
* @return the index into the text of the start of the selection ≥ 0 |
|
*/ |
|
public int getSelectionStart() { |
|
return JTextComponent.this.getSelectionStart(); |
|
} |
|
/** |
|
* Returns the end offset within the selected text. |
|
* If there is no selection, but there is |
|
* a caret, the start and end offsets will be the same. |
|
* Return 0 if the text is empty, or the caret position |
|
* if no selection. |
|
* |
|
* @return the index into the text of the end of the selection ≥ 0 |
|
*/ |
|
public int getSelectionEnd() { |
|
return JTextComponent.this.getSelectionEnd(); |
|
} |
|
/** |
|
* Returns the portion of the text that is selected. |
|
* |
|
* @return the text, null if no selection |
|
*/ |
|
public String getSelectedText() { |
|
return JTextComponent.this.getSelectedText(); |
|
} |
|
/** |
|
* IndexedSegment extends Segment adding the offset into the |
|
* the model the <code>Segment</code> was asked for. |
|
*/ |
|
private class IndexedSegment extends Segment { |
|
/** |
|
* Offset into the model that the position represents. |
|
*/ |
|
public int modelOffset; |
|
} |
|
// TIGER - 4170173 |
|
/** |
|
* Returns the String at a given index. Whitespace |
|
* between words is treated as a word. |
|
* |
|
* @param part the CHARACTER, WORD, or SENTENCE to retrieve |
|
* @param index an index within the text |
|
* @return the letter, word, or sentence. |
|
* |
|
*/ |
|
public String getAtIndex(int part, int index) { |
|
return getAtIndex(part, index, 0); |
|
} |
|
/** |
|
* Returns the String after a given index. Whitespace |
|
* between words is treated as a word. |
|
* |
|
* @param part the CHARACTER, WORD, or SENTENCE to retrieve |
|
* @param index an index within the text |
|
* @return the letter, word, or sentence. |
|
*/ |
|
public String getAfterIndex(int part, int index) { |
|
return getAtIndex(part, index, 1); |
|
} |
|
/** |
|
* Returns the String before a given index. Whitespace |
|
* between words is treated a word. |
|
* |
|
* @param part the CHARACTER, WORD, or SENTENCE to retrieve |
|
* @param index an index within the text |
|
* @return the letter, word, or sentence. |
|
*/ |
|
public String getBeforeIndex(int part, int index) { |
|
return getAtIndex(part, index, -1); |
|
} |
|
/** |
|
* Gets the word, sentence, or character at <code>index</code>. |
|
* If <code>direction</code> is non-null this will find the |
|
* next/previous word/sentence/character. |
|
*/ |
|
private String getAtIndex(int part, int index, int direction) { |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readLock(); |
|
} |
|
try { |
|
if (index < 0 || index >= model.getLength()) { |
|
return null; |
|
} |
|
switch (part) { |
|
case AccessibleText.CHARACTER: |
|
if (index + direction < model.getLength() && |
|
index + direction >= 0) { |
|
return model.getText(index + direction, 1); |
|
} |
|
break; |
|
case AccessibleText.WORD: |
|
case AccessibleText.SENTENCE: |
|
IndexedSegment seg = getSegmentAt(part, index); |
|
if (seg != null) { |
|
if (direction != 0) { |
|
int next; |
|
if (direction < 0) { |
|
next = seg.modelOffset - 1; |
|
} |
|
else { |
|
next = seg.modelOffset + direction * seg.count; |
|
} |
|
if (next >= 0 && next <= model.getLength()) { |
|
seg = getSegmentAt(part, next); |
|
} |
|
else { |
|
seg = null; |
|
} |
|
} |
|
if (seg != null) { |
|
return new String(seg.array, seg.offset, |
|
seg.count); |
|
} |
|
} |
|
break; |
|
default: |
|
break; |
|
} |
|
} catch (BadLocationException e) { |
|
} finally { |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readUnlock(); |
|
} |
|
} |
|
return null; |
|
} |
|
/* |
|
* Returns the paragraph element for the specified index. |
|
*/ |
|
private Element getParagraphElement(int index) { |
|
if (model instanceof PlainDocument ) { |
|
PlainDocument sdoc = (PlainDocument)model; |
|
return sdoc.getParagraphElement(index); |
|
} else if (model instanceof StyledDocument) { |
|
StyledDocument sdoc = (StyledDocument)model; |
|
return sdoc.getParagraphElement(index); |
|
} else { |
|
Element para; |
|
for (para = model.getDefaultRootElement(); ! para.isLeaf(); ) { |
|
int pos = para.getElementIndex(index); |
|
para = para.getElement(pos); |
|
} |
|
if (para == null) { |
|
return null; |
|
} |
|
return para.getParentElement(); |
|
} |
|
} |
|
/* |
|
* Returns a <code>Segment</code> containing the paragraph text |
|
* at <code>index</code>, or null if <code>index</code> isn't |
|
* valid. |
|
*/ |
|
private IndexedSegment getParagraphElementText(int index) |
|
throws BadLocationException { |
|
Element para = getParagraphElement(index); |
|
if (para != null) { |
|
IndexedSegment segment = new IndexedSegment(); |
|
try { |
|
int length = para.getEndOffset() - para.getStartOffset(); |
|
model.getText(para.getStartOffset(), length, segment); |
|
} catch (BadLocationException e) { |
|
return null; |
|
} |
|
segment.modelOffset = para.getStartOffset(); |
|
return segment; |
|
} |
|
return null; |
|
} |
|
/** |
|
* Returns the Segment at <code>index</code> representing either |
|
* the paragraph or sentence as identified by <code>part</code>, or |
|
* null if a valid paragraph/sentence can't be found. The offset |
|
* will point to the start of the word/sentence in the array, and |
|
* the modelOffset will point to the location of the word/sentence |
|
* in the model. |
|
*/ |
|
private IndexedSegment getSegmentAt(int part, int index) throws |
|
BadLocationException { |
|
IndexedSegment seg = getParagraphElementText(index); |
|
if (seg == null) { |
|
return null; |
|
} |
|
BreakIterator iterator; |
|
switch (part) { |
|
case AccessibleText.WORD: |
|
iterator = BreakIterator.getWordInstance(getLocale()); |
|
break; |
|
case AccessibleText.SENTENCE: |
|
iterator = BreakIterator.getSentenceInstance(getLocale()); |
|
break; |
|
default: |
|
return null; |
|
} |
|
seg.first(); |
|
iterator.setText(seg); |
|
int end = iterator.following(index - seg.modelOffset + seg.offset); |
|
if (end == BreakIterator.DONE) { |
|
return null; |
|
} |
|
if (end > seg.offset + seg.count) { |
|
return null; |
|
} |
|
int begin = iterator.previous(); |
|
if (begin == BreakIterator.DONE || |
|
begin >= seg.offset + seg.count) { |
|
return null; |
|
} |
|
seg.modelOffset = seg.modelOffset + begin - seg.offset; |
|
seg.offset = begin; |
|
seg.count = end - begin; |
|
return seg; |
|
} |
|
// begin AccessibleEditableText methods ----- |
|
/** |
|
* Returns the AccessibleEditableText interface for |
|
* this text component. |
|
* |
|
* @return the AccessibleEditableText interface |
|
* @since 1.4 |
|
*/ |
|
public AccessibleEditableText getAccessibleEditableText() { |
|
return this; |
|
} |
|
/** |
|
* Sets the text contents to the specified string. |
|
* |
|
* @param s the string to set the text contents |
|
* @since 1.4 |
|
*/ |
|
public void setTextContents(String s) { |
|
JTextComponent.this.setText(s); |
|
} |
|
/** |
|
* Inserts the specified string at the given index |
|
* |
|
* @param index the index in the text where the string will |
|
* be inserted |
|
* @param s the string to insert in the text |
|
* @since 1.4 |
|
*/ |
|
public void insertTextAtIndex(int index, String s) { |
|
Document doc = JTextComponent.this.getDocument(); |
|
if (doc != null) { |
|
try { |
|
if (s != null && s.length() > 0) { |
|
boolean composedTextSaved = saveComposedText(index); |
|
doc.insertString(index, s, null); |
|
if (composedTextSaved) { |
|
restoreComposedText(); |
|
} |
|
} |
|
} catch (BadLocationException e) { |
|
UIManager.getLookAndFeel().provideErrorFeedback(JTextComponent.this); |
|
} |
|
} |
|
} |
|
/** |
|
* Returns the text string between two indices. |
|
* |
|
* @param startIndex the starting index in the text |
|
* @param endIndex the ending index in the text |
|
* @return the text string between the indices |
|
* @since 1.4 |
|
*/ |
|
public String getTextRange(int startIndex, int endIndex) { |
|
String txt = null; |
|
int p0 = Math.min(startIndex, endIndex); |
|
int p1 = Math.max(startIndex, endIndex); |
|
if (p0 != p1) { |
|
try { |
|
Document doc = JTextComponent.this.getDocument(); |
|
txt = doc.getText(p0, p1 - p0); |
|
} catch (BadLocationException e) { |
|
throw new IllegalArgumentException(e.getMessage()); |
|
} |
|
} |
|
return txt; |
|
} |
|
/** |
|
* Deletes the text between two indices |
|
* |
|
* @param startIndex the starting index in the text |
|
* @param endIndex the ending index in the text |
|
* @since 1.4 |
|
*/ |
|
public void delete(int startIndex, int endIndex) { |
|
if (isEditable() && isEnabled()) { |
|
try { |
|
int p0 = Math.min(startIndex, endIndex); |
|
int p1 = Math.max(startIndex, endIndex); |
|
if (p0 != p1) { |
|
Document doc = getDocument(); |
|
doc.remove(p0, p1 - p0); |
|
} |
|
} catch (BadLocationException e) { |
|
} |
|
} else { |
|
UIManager.getLookAndFeel().provideErrorFeedback(JTextComponent.this); |
|
} |
|
} |
|
/** |
|
* Cuts the text between two indices into the system clipboard. |
|
* |
|
* @param startIndex the starting index in the text |
|
* @param endIndex the ending index in the text |
|
* @since 1.4 |
|
*/ |
|
public void cut(int startIndex, int endIndex) { |
|
selectText(startIndex, endIndex); |
|
JTextComponent.this.cut(); |
|
} |
|
/** |
|
* Pastes the text from the system clipboard into the text |
|
* starting at the specified index. |
|
* |
|
* @param startIndex the starting index in the text |
|
* @since 1.4 |
|
*/ |
|
public void paste(int startIndex) { |
|
setCaretPosition(startIndex); |
|
JTextComponent.this.paste(); |
|
} |
|
/** |
|
* Replaces the text between two indices with the specified |
|
* string. |
|
* |
|
* @param startIndex the starting index in the text |
|
* @param endIndex the ending index in the text |
|
* @param s the string to replace the text between two indices |
|
* @since 1.4 |
|
*/ |
|
public void replaceText(int startIndex, int endIndex, String s) { |
|
selectText(startIndex, endIndex); |
|
JTextComponent.this.replaceSelection(s); |
|
} |
|
/** |
|
* Selects the text between two indices. |
|
* |
|
* @param startIndex the starting index in the text |
|
* @param endIndex the ending index in the text |
|
* @since 1.4 |
|
*/ |
|
public void selectText(int startIndex, int endIndex) { |
|
JTextComponent.this.select(startIndex, endIndex); |
|
} |
|
/** |
|
* Sets attributes for the text between two indices. |
|
* |
|
* @param startIndex the starting index in the text |
|
* @param endIndex the ending index in the text |
|
* @param as the attribute set |
|
* @see AttributeSet |
|
* @since 1.4 |
|
*/ |
|
public void setAttributes(int startIndex, int endIndex, |
|
AttributeSet as) { |
|
// Fixes bug 4487492 |
|
Document doc = JTextComponent.this.getDocument(); |
|
if (doc != null && doc instanceof StyledDocument) { |
|
StyledDocument sDoc = (StyledDocument)doc; |
|
int offset = startIndex; |
|
int length = endIndex - startIndex; |
|
sDoc.setCharacterAttributes(offset, length, as, true); |
|
} |
|
} |
|
// ----- end AccessibleEditableText methods |
|
// ----- begin AccessibleExtendedText methods |
|
// Probably should replace the helper method getAtIndex() to return |
|
// instead an AccessibleTextSequence also for LINE & ATTRIBUTE_RUN |
|
// and then make the AccessibleText methods get[At|After|Before]Point |
|
// call this new method instead and return only the string portion |
|
/** |
|
* Returns the AccessibleTextSequence at a given <code>index</code>. |
|
* If <code>direction</code> is non-null this will find the |
|
* next/previous word/sentence/character. |
|
* |
|
* @param part the <code>CHARACTER</code>, <code>WORD</code>, |
|
* <code>SENTENCE</code>, <code>LINE</code> or |
|
* <code>ATTRIBUTE_RUN</code> to retrieve |
|
* @param index an index within the text |
|
* @param direction is either -1, 0, or 1 |
|
* @return an <code>AccessibleTextSequence</code> specifying the text |
|
* if <code>part</code> and <code>index</code> are valid. Otherwise, |
|
* <code>null</code> is returned. |
|
* |
|
* @see javax.accessibility.AccessibleText#CHARACTER |
|
* @see javax.accessibility.AccessibleText#WORD |
|
* @see javax.accessibility.AccessibleText#SENTENCE |
|
* @see javax.accessibility.AccessibleExtendedText#LINE |
|
* @see javax.accessibility.AccessibleExtendedText#ATTRIBUTE_RUN |
|
* |
|
* @since 1.6 |
|
*/ |
|
private AccessibleTextSequence getSequenceAtIndex(int part, |
|
int index, int direction) { |
|
if (index < 0 || index >= model.getLength()) { |
|
return null; |
|
} |
|
if (direction < -1 || direction > 1) { |
|
return null; // direction must be 1, 0, or -1 |
|
} |
|
switch (part) { |
|
case AccessibleText.CHARACTER: |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readLock(); |
|
} |
|
AccessibleTextSequence charSequence = null; |
|
try { |
|
if (index + direction < model.getLength() && |
|
index + direction >= 0) { |
|
charSequence = |
|
new AccessibleTextSequence(index + direction, |
|
index + direction + 1, |
|
model.getText(index + direction, 1)); |
|
} |
|
} catch (BadLocationException e) { |
|
// we are intentionally silent; our contract says we return |
|
// null if there is any failure in this method |
|
} finally { |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readUnlock(); |
|
} |
|
} |
|
return charSequence; |
|
case AccessibleText.WORD: |
|
case AccessibleText.SENTENCE: |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readLock(); |
|
} |
|
AccessibleTextSequence rangeSequence = null; |
|
try { |
|
IndexedSegment seg = getSegmentAt(part, index); |
|
if (seg != null) { |
|
if (direction != 0) { |
|
int next; |
|
if (direction < 0) { |
|
next = seg.modelOffset - 1; |
|
} |
|
else { |
|
next = seg.modelOffset + seg.count; |
|
} |
|
if (next >= 0 && next <= model.getLength()) { |
|
seg = getSegmentAt(part, next); |
|
} |
|
else { |
|
seg = null; |
|
} |
|
} |
|
if (seg != null && |
|
(seg.offset + seg.count) <= model.getLength()) { |
|
rangeSequence = |
|
new AccessibleTextSequence (seg.offset, |
|
seg.offset + seg.count, |
|
new String(seg.array, seg.offset, seg.count)); |
|
} // else we leave rangeSequence set to null |
|
} |
|
} catch(BadLocationException e) { |
|
// we are intentionally silent; our contract says we return |
|
// null if there is any failure in this method |
|
} finally { |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readUnlock(); |
|
} |
|
} |
|
return rangeSequence; |
|
case AccessibleExtendedText.LINE: |
|
AccessibleTextSequence lineSequence = null; |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readLock(); |
|
} |
|
try { |
|
int startIndex = |
|
Utilities.getRowStart(JTextComponent.this, index); |
|
int endIndex = |
|
Utilities.getRowEnd(JTextComponent.this, index); |
|
if (startIndex >= 0 && endIndex >= startIndex) { |
|
if (direction == 0) { |
|
lineSequence = |
|
new AccessibleTextSequence(startIndex, endIndex, |
|
model.getText(startIndex, |
|
endIndex - startIndex + 1)); |
|
} else if (direction == -1 && startIndex > 0) { |
|
endIndex = |
|
Utilities.getRowEnd(JTextComponent.this, |
|
startIndex - 1); |
|
startIndex = |
|
Utilities.getRowStart(JTextComponent.this, |
|
startIndex - 1); |
|
if (startIndex >= 0 && endIndex >= startIndex) { |
|
lineSequence = |
|
new AccessibleTextSequence(startIndex, |
|
endIndex, |
|
model.getText(startIndex, |
|
endIndex - startIndex + 1)); |
|
} |
|
} else if (direction == 1 && |
|
endIndex < model.getLength()) { |
|
startIndex = |
|
Utilities.getRowStart(JTextComponent.this, |
|
endIndex + 1); |
|
endIndex = |
|
Utilities.getRowEnd(JTextComponent.this, |
|
endIndex + 1); |
|
if (startIndex >= 0 && endIndex >= startIndex) { |
|
lineSequence = |
|
new AccessibleTextSequence(startIndex, |
|
endIndex, model.getText(startIndex, |
|
endIndex - startIndex + 1)); |
|
} |
|
} |
|
// already validated 'direction' above... |
|
} |
|
} catch(BadLocationException e) { |
|
// we are intentionally silent; our contract says we return |
|
// null if there is any failure in this method |
|
} finally { |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readUnlock(); |
|
} |
|
} |
|
return lineSequence; |
|
case AccessibleExtendedText.ATTRIBUTE_RUN: |
|
// assumptions: (1) that all characters in a single element |
|
// share the same attribute set; (2) that adjacent elements |
|
// *may* share the same attribute set |
|
int attributeRunStartIndex, attributeRunEndIndex; |
|
String runText = null; |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readLock(); |
|
} |
|
try { |
|
attributeRunStartIndex = attributeRunEndIndex = |
|
Integer.MIN_VALUE; |
|
int tempIndex = index; |
|
switch (direction) { |
|
case -1: |
|
// going backwards, so find left edge of this run - |
|
// that'll be the end of the previous run |
|
// (off-by-one counting) |
|
attributeRunEndIndex = getRunEdge(index, direction); |
|
// now set ourselves up to find the left edge of the |
|
// prev. run |
|
tempIndex = attributeRunEndIndex - 1; |
|
break; |
|
case 1: |
|
// going forward, so find right edge of this run - |
|
// that'll be the start of the next run |
|
// (off-by-one counting) |
|
attributeRunStartIndex = getRunEdge(index, direction); |
|
// now set ourselves up to find the right edge of the |
|
// next run |
|
tempIndex = attributeRunStartIndex; |
|
break; |
|
case 0: |
|
// interested in the current run, so nothing special to |
|
// set up in advance... |
|
break; |
|
default: |
|
// only those three values of direction allowed... |
|
throw new AssertionError(direction); |
|
} |
|
// set the unset edge; if neither set then we're getting |
|
// both edges of the current run around our 'index' |
|
attributeRunStartIndex = |
|
(attributeRunStartIndex != Integer.MIN_VALUE) ? |
|
attributeRunStartIndex : getRunEdge(tempIndex, -1); |
|
attributeRunEndIndex = |
|
(attributeRunEndIndex != Integer.MIN_VALUE) ? |
|
attributeRunEndIndex : getRunEdge(tempIndex, 1); |
|
runText = model.getText(attributeRunStartIndex, |
|
attributeRunEndIndex - |
|
attributeRunStartIndex); |
|
} catch (BadLocationException e) { |
|
// we are intentionally silent; our contract says we return |
|
// null if there is any failure in this method |
|
return null; |
|
} finally { |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readUnlock(); |
|
} |
|
} |
|
return new AccessibleTextSequence(attributeRunStartIndex, |
|
attributeRunEndIndex, |
|
runText); |
|
default: |
|
break; |
|
} |
|
return null; |
|
} |
|
/** |
|
* Starting at text position <code>index</code>, and going in |
|
* <code>direction</code>, return the edge of run that shares the |
|
* same <code>AttributeSet</code> and parent element as those at |
|
* <code>index</code>. |
|
* |
|
* Note: we assume the document is already locked... |
|
*/ |
|
private int getRunEdge(int index, int direction) throws |
|
BadLocationException { |
|
if (index < 0 || index >= model.getLength()) { |
|
throw new BadLocationException("Location out of bounds", index); |
|
} |
|
// locate the Element at index |
|
Element indexElement; |
|
// locate the Element at our index/offset |
|
int elementIndex = -1; // test for initialization |
|
for (indexElement = model.getDefaultRootElement(); |
|
! indexElement.isLeaf(); ) { |
|
elementIndex = indexElement.getElementIndex(index); |
|
indexElement = indexElement.getElement(elementIndex); |
|
} |
|
if (elementIndex == -1) { |
|
throw new AssertionError(index); |
|
} |
|
// cache the AttributeSet and parentElement atindex |
|
AttributeSet indexAS = indexElement.getAttributes(); |
|
Element parent = indexElement.getParentElement(); |
|
// find the first Element before/after ours w/the same AttributeSet |
|
// if we are already at edge of the first element in our parent |
|
// then return that edge |
|
Element edgeElement; |
|
switch (direction) { |
|
case -1: |
|
case 1: |
|
int edgeElementIndex = elementIndex; |
|
int elementCount = parent.getElementCount(); |
|
while ((edgeElementIndex + direction) > 0 && |
|
((edgeElementIndex + direction) < elementCount) && |
|
parent.getElement(edgeElementIndex |
|
+ direction).getAttributes().isEqual(indexAS)) { |
|
edgeElementIndex += direction; |
|
} |
|
edgeElement = parent.getElement(edgeElementIndex); |
|
break; |
|
default: |
|
throw new AssertionError(direction); |
|
} |
|
switch (direction) { |
|
case -1: |
|
return edgeElement.getStartOffset(); |
|
case 1: |
|
return edgeElement.getEndOffset(); |
|
default: |
|
// we already caught this case earlier; this is to satisfy |
|
// the compiler... |
|
return Integer.MIN_VALUE; |
|
} |
|
} |
|
// getTextRange() not needed; defined in AccessibleEditableText |
|
/** |
|
* Returns the <code>AccessibleTextSequence</code> at a given |
|
* <code>index</code>. |
|
* |
|
* @param part the <code>CHARACTER</code>, <code>WORD</code>, |
|
* <code>SENTENCE</code>, <code>LINE</code> or |
|
* <code>ATTRIBUTE_RUN</code> to retrieve |
|
* @param index an index within the text |
|
* @return an <code>AccessibleTextSequence</code> specifying the text if |
|
* <code>part</code> and <code>index</code> are valid. Otherwise, |
|
* <code>null</code> is returned |
|
* |
|
* @see javax.accessibility.AccessibleText#CHARACTER |
|
* @see javax.accessibility.AccessibleText#WORD |
|
* @see javax.accessibility.AccessibleText#SENTENCE |
|
* @see javax.accessibility.AccessibleExtendedText#LINE |
|
* @see javax.accessibility.AccessibleExtendedText#ATTRIBUTE_RUN |
|
* |
|
* @since 1.6 |
|
*/ |
|
public AccessibleTextSequence getTextSequenceAt(int part, int index) { |
|
return getSequenceAtIndex(part, index, 0); |
|
} |
|
/** |
|
* Returns the <code>AccessibleTextSequence</code> after a given |
|
* <code>index</code>. |
|
* |
|
* @param part the <code>CHARACTER</code>, <code>WORD</code>, |
|
* <code>SENTENCE</code>, <code>LINE</code> or |
|
* <code>ATTRIBUTE_RUN</code> to retrieve |
|
* @param index an index within the text |
|
* @return an <code>AccessibleTextSequence</code> specifying the text |
|
* if <code>part</code> and <code>index</code> are valid. Otherwise, |
|
* <code>null</code> is returned |
|
* |
|
* @see javax.accessibility.AccessibleText#CHARACTER |
|
* @see javax.accessibility.AccessibleText#WORD |
|
* @see javax.accessibility.AccessibleText#SENTENCE |
|
* @see javax.accessibility.AccessibleExtendedText#LINE |
|
* @see javax.accessibility.AccessibleExtendedText#ATTRIBUTE_RUN |
|
* |
|
* @since 1.6 |
|
*/ |
|
public AccessibleTextSequence getTextSequenceAfter(int part, int index) { |
|
return getSequenceAtIndex(part, index, 1); |
|
} |
|
/** |
|
* Returns the <code>AccessibleTextSequence</code> before a given |
|
* <code>index</code>. |
|
* |
|
* @param part the <code>CHARACTER</code>, <code>WORD</code>, |
|
* <code>SENTENCE</code>, <code>LINE</code> or |
|
* <code>ATTRIBUTE_RUN</code> to retrieve |
|
* @param index an index within the text |
|
* @return an <code>AccessibleTextSequence</code> specifying the text |
|
* if <code>part</code> and <code>index</code> are valid. Otherwise, |
|
* <code>null</code> is returned |
|
* |
|
* @see javax.accessibility.AccessibleText#CHARACTER |
|
* @see javax.accessibility.AccessibleText#WORD |
|
* @see javax.accessibility.AccessibleText#SENTENCE |
|
* @see javax.accessibility.AccessibleExtendedText#LINE |
|
* @see javax.accessibility.AccessibleExtendedText#ATTRIBUTE_RUN |
|
* |
|
* @since 1.6 |
|
*/ |
|
public AccessibleTextSequence getTextSequenceBefore(int part, int index) { |
|
return getSequenceAtIndex(part, index, -1); |
|
} |
|
/** |
|
* Returns the <code>Rectangle</code> enclosing the text between |
|
* two indicies. |
|
* |
|
* @param startIndex the start index in the text |
|
* @param endIndex the end index in the text |
|
* @return the bounding rectangle of the text if the indices are valid. |
|
* Otherwise, <code>null</code> is returned |
|
* |
|
* @since 1.6 |
|
*/ |
|
public Rectangle getTextBounds(int startIndex, int endIndex) { |
|
if (startIndex < 0 || startIndex > model.getLength()-1 || |
|
endIndex < 0 || endIndex > model.getLength()-1 || |
|
startIndex > endIndex) { |
|
return null; |
|
} |
|
TextUI ui = getUI(); |
|
if (ui == null) { |
|
return null; |
|
} |
|
Rectangle rect = null; |
|
Rectangle alloc = getRootEditorRect(); |
|
if (alloc == null) { |
|
return null; |
|
} |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readLock(); |
|
} |
|
try { |
|
View rootView = ui.getRootView(JTextComponent.this); |
|
if (rootView != null) { |
|
Shape bounds = rootView.modelToView(startIndex, |
|
Position.Bias.Forward, endIndex, |
|
Position.Bias.Backward, alloc); |
|
rect = (bounds instanceof Rectangle) ? |
|
(Rectangle)bounds : bounds.getBounds(); |
|
} |
|
} catch (BadLocationException e) { |
|
} finally { |
|
if (model instanceof AbstractDocument) { |
|
((AbstractDocument)model).readUnlock(); |
|
} |
|
} |
|
return rect; |
|
} |
|
// ----- end AccessibleExtendedText methods |
|
// --- interface AccessibleAction methods ------------------------ |
|
public AccessibleAction getAccessibleAction() { |
|
return this; |
|
} |
|
/** |
|
* Returns the number of accessible actions available in this object |
|
* If there are more than one, the first one is considered the |
|
* "default" action of the object. |
|
* |
|
* @return the zero-based number of Actions in this object |
|
* @since 1.4 |
|
*/ |
|
public int getAccessibleActionCount() { |
|
Action [] actions = JTextComponent.this.getActions(); |
|
return actions.length; |
|
} |
|
/** |
|
* Returns a description of the specified action of the object. |
|
* |
|
* @param i zero-based index of the actions |
|
* @return a String description of the action |
|
* @see #getAccessibleActionCount |
|
* @since 1.4 |
|
*/ |
|
public String getAccessibleActionDescription(int i) { |
|
Action [] actions = JTextComponent.this.getActions(); |
|
if (i < 0 || i >= actions.length) { |
|
return null; |
|
} |
|
return (String)actions[i].getValue(Action.NAME); |
|
} |
|
/** |
|
* Performs the specified Action on the object |
|
* |
|
* @param i zero-based index of actions |
|
* @return true if the action was performed; otherwise false. |
|
* @see #getAccessibleActionCount |
|
* @since 1.4 |
|
*/ |
|
public boolean doAccessibleAction(int i) { |
|
Action [] actions = JTextComponent.this.getActions(); |
|
if (i < 0 || i >= actions.length) { |
|
return false; |
|
} |
|
ActionEvent ae = |
|
new ActionEvent(JTextComponent.this, |
|
ActionEvent.ACTION_PERFORMED, null, |
|
EventQueue.getMostRecentEventTime(), |
|
getCurrentEventModifiers()); |
|
actions[i].actionPerformed(ae); |
|
return true; |
|
} |
|
// ----- end AccessibleAction methods |
|
} |
|
// --- serialization --------------------------------------------- |
|
private void readObject(ObjectInputStream s) |
|
throws IOException, ClassNotFoundException |
|
{ |
|
s.defaultReadObject(); |
|
caretEvent = new MutableCaretEvent(this); |
|
addMouseListener(caretEvent); |
|
addFocusListener(caretEvent); |
|
} |
|
// --- member variables ---------------------------------- |
|
/** |
|
* The document model. |
|
*/ |
|
private Document model; |
|
/** |
|
* The caret used to display the insert position |
|
* and navigate throughout the document. |
|
* |
|
* PENDING(prinz) |
|
* This should be serializable, default installed |
|
* by UI. |
|
*/ |
|
private transient Caret caret; |
|
/** |
|
* Object responsible for restricting the cursor navigation. |
|
*/ |
|
private NavigationFilter navigationFilter; |
|
/** |
|
* The object responsible for managing highlights. |
|
* |
|
* PENDING(prinz) |
|
* This should be serializable, default installed |
|
* by UI. |
|
*/ |
|
private transient Highlighter highlighter; |
|
/** |
|
* The current key bindings in effect. |
|
* |
|
* PENDING(prinz) |
|
* This should be serializable, default installed |
|
* by UI. |
|
*/ |
|
private transient Keymap keymap; |
|
private transient MutableCaretEvent caretEvent; |
|
private Color caretColor; |
|
private Color selectionColor; |
|
private Color selectedTextColor; |
|
private Color disabledTextColor; |
|
private boolean editable; |
|
private Insets margin; |
|
private char focusAccelerator; |
|
private boolean dragEnabled; |
|
/** |
|
* The drop mode for this component. |
|
*/ |
|
private DropMode dropMode = DropMode.USE_SELECTION; |
|
/** |
|
* The drop location. |
|
*/ |
|
private transient DropLocation dropLocation; |
|
/** |
|
* Represents a drop location for <code>JTextComponent</code>s. |
|
* |
|
* @see #getDropLocation |
|
* @since 1.6 |
|
*/ |
|
public static final class DropLocation extends TransferHandler.DropLocation { |
|
private final int index; |
|
private final Position.Bias bias; |
|
private DropLocation(Point p, int index, Position.Bias bias) { |
|
super(p); |
|
this.index = index; |
|
this.bias = bias; |
|
} |
|
/** |
|
* Returns the index where dropped data should be inserted into the |
|
* associated component. This index represents a position between |
|
* characters, as would be interpreted by a caret. |
|
* |
|
* @return the drop index |
|
*/ |
|
public int getIndex() { |
|
return index; |
|
} |
|
/** |
|
* Returns the bias for the drop index. |
|
* |
|
* @return the drop bias |
|
*/ |
|
public Position.Bias getBias() { |
|
return bias; |
|
} |
|
/** |
|
* Returns a string representation of this drop location. |
|
* This method is intended to be used for debugging purposes, |
|
* and the content and format of the returned string may vary |
|
* between implementations. |
|
* |
|
* @return a string representation of this drop location |
|
*/ |
|
public String toString() { |
|
return getClass().getName() |
|
+ "[dropPoint=" + getDropPoint() + "," |
|
+ "index=" + index + "," |
|
+ "bias=" + bias + "]"; |
|
} |
|
} |
|
/** |
|
* TransferHandler used if one hasn't been supplied by the UI. |
|
*/ |
|
private static DefaultTransferHandler defaultTransferHandler; |
|
/** |
|
* Maps from class name to Boolean indicating if |
|
* <code>processInputMethodEvent</code> has been overriden. |
|
*/ |
|
private static Cache<Class<?>,Boolean> METHOD_OVERRIDDEN |
|
= new Cache<Class<?>,Boolean>(Cache.Kind.WEAK, Cache.Kind.STRONG) { |
|
/** |
|
* Returns {@code true} if the specified {@code type} extends {@link JTextComponent} |
|
* and the {@link JTextComponent#processInputMethodEvent} method is overridden. |
|
*/ |
|
@Override |
|
public Boolean create(final Class<?> type) { |
|
if (JTextComponent.class == type) { |
|
return Boolean.FALSE; |
|
} |
|
if (get(type.getSuperclass())) { |
|
return Boolean.TRUE; |
|
} |
|
return AccessController.doPrivileged( |
|
new PrivilegedAction<Boolean>() { |
|
public Boolean run() { |
|
try { |
|
type.getDeclaredMethod("processInputMethodEvent", InputMethodEvent.class); |
|
return Boolean.TRUE; |
|
} catch (NoSuchMethodException exception) { |
|
return Boolean.FALSE; |
|
} |
|
} |
|
}); |
|
} |
|
}; |
|
/** |
|
* Returns a string representation of this <code>JTextComponent</code>. |
|
* This method is intended to be used only for debugging purposes, and the |
|
* content and format of the returned string may vary between |
|
* implementations. The returned string may be empty but may not |
|
* be <code>null</code>. |
|
* <P> |
|
* Overriding <code>paramString</code> to provide information about the |
|
* specific new aspects of the JFC components. |
|
* |
|
* @return a string representation of this <code>JTextComponent</code> |
|
*/ |
|
protected String paramString() { |
|
String editableString = (editable ? |
|
"true" : "false"); |
|
String caretColorString = (caretColor != null ? |
|
caretColor.toString() : ""); |
|
String selectionColorString = (selectionColor != null ? |
|
selectionColor.toString() : ""); |
|
String selectedTextColorString = (selectedTextColor != null ? |
|
selectedTextColor.toString() : ""); |
|
String disabledTextColorString = (disabledTextColor != null ? |
|
disabledTextColor.toString() : ""); |
|
String marginString = (margin != null ? |
|
margin.toString() : ""); |
|
return super.paramString() + |
|
",caretColor=" + caretColorString + |
|
",disabledTextColor=" + disabledTextColorString + |
|
",editable=" + editableString + |
|
",margin=" + marginString + |
|
",selectedTextColor=" + selectedTextColorString + |
|
",selectionColor=" + selectionColorString; |
|
} |
|
/** |
|
* A Simple TransferHandler that exports the data as a String, and |
|
* imports the data from the String clipboard. This is only used |
|
* if the UI hasn't supplied one, which would only happen if someone |
|
* hasn't subclassed Basic. |
|
*/ |
|
static class DefaultTransferHandler extends TransferHandler implements |
|
UIResource { |
|
public void exportToClipboard(JComponent comp, Clipboard clipboard, |
|
int action) throws IllegalStateException { |
|
if (comp instanceof JTextComponent) { |
|
JTextComponent text = (JTextComponent)comp; |
|
int p0 = text.getSelectionStart(); |
|
int p1 = text.getSelectionEnd(); |
|
if (p0 != p1) { |
|
try { |
|
Document doc = text.getDocument(); |
|
String srcData = doc.getText(p0, p1 - p0); |
|
StringSelection contents =new StringSelection(srcData); |
|
// this may throw an IllegalStateException, |
|
// but it will be caught and handled in the |
|
// action that invoked this method |
|
clipboard.setContents(contents, null); |
|
if (action == TransferHandler.MOVE) { |
|
doc.remove(p0, p1 - p0); |
|
} |
|
} catch (BadLocationException ble) {} |
|
} |
|
} |
|
} |
|
public boolean importData(JComponent comp, Transferable t) { |
|
if (comp instanceof JTextComponent) { |
|
DataFlavor flavor = getFlavor(t.getTransferDataFlavors()); |
|
if (flavor != null) { |
|
InputContext ic = comp.getInputContext(); |
|
if (ic != null) { |
|
ic.endComposition(); |
|
} |
|
try { |
|
String data = (String)t.getTransferData(flavor); |
|
((JTextComponent)comp).replaceSelection(data); |
|
return true; |
|
} catch (UnsupportedFlavorException ufe) { |
|
} catch (IOException ioe) { |
|
} |
|
} |
|
} |
|
return false; |
|
} |
|
public boolean canImport(JComponent comp, |
|
DataFlavor[] transferFlavors) { |
|
JTextComponent c = (JTextComponent)comp; |
|
if (!(c.isEditable() && c.isEnabled())) { |
|
return false; |
|
} |
|
return (getFlavor(transferFlavors) != null); |
|
} |
|
public int getSourceActions(JComponent c) { |
|
return NONE; |
|
} |
|
private DataFlavor getFlavor(DataFlavor[] flavors) { |
|
if (flavors != null) { |
|
for (DataFlavor flavor : flavors) { |
|
if (flavor.equals(DataFlavor.stringFlavor)) { |
|
return flavor; |
|
} |
|
} |
|
} |
|
return null; |
|
} |
|
} |
|
/** |
|
* Returns the JTextComponent that most recently had focus. The returned |
|
* value may currently have focus. |
|
*/ |
|
static final JTextComponent getFocusedComponent() { |
|
return (JTextComponent)AppContext.getAppContext(). |
|
get(FOCUSED_COMPONENT); |
|
} |
|
private int getCurrentEventModifiers() { |
|
int modifiers = 0; |
|
AWTEvent currentEvent = EventQueue.getCurrentEvent(); |
|
if (currentEvent instanceof InputEvent) { |
|
modifiers = ((InputEvent)currentEvent).getModifiers(); |
|
} else if (currentEvent instanceof ActionEvent) { |
|
modifiers = ((ActionEvent)currentEvent).getModifiers(); |
|
} |
|
return modifiers; |
|
} |
|
private static final Object KEYMAP_TABLE = |
|
new StringBuilder("JTextComponent_KeymapTable"); |
|
// |
|
// member variables used for on-the-spot input method |
|
// editing style support |
|
// |
|
private transient InputMethodRequests inputMethodRequestsHandler; |
|
private SimpleAttributeSet composedTextAttribute; |
|
private String composedTextContent; |
|
private Position composedTextStart; |
|
private Position composedTextEnd; |
|
private Position latestCommittedTextStart; |
|
private Position latestCommittedTextEnd; |
|
private ComposedTextCaret composedTextCaret; |
|
private transient Caret originalCaret; |
|
/** |
|
* Set to true after the check for the override of processInputMethodEvent |
|
* has been checked. |
|
*/ |
|
private boolean checkedInputOverride; |
|
private boolean needToSendKeyTypedEvent; |
|
static class DefaultKeymap implements Keymap { |
|
DefaultKeymap(String nm, Keymap parent) { |
|
this.nm = nm; |
|
this.parent = parent; |
|
bindings = new Hashtable<KeyStroke, Action>(); |
|
} |
|
/** |
|
* Fetch the default action to fire if a |
|
* key is typed (ie a KEY_TYPED KeyEvent is received) |
|
* and there is no binding for it. Typically this |
|
* would be some action that inserts text so that |
|
* the keymap doesn't require an action for each |
|
* possible key. |
|
*/ |
|
public Action getDefaultAction() { |
|
if (defaultAction != null) { |
|
return defaultAction; |
|
} |
|
return (parent != null) ? parent.getDefaultAction() : null; |
|
} |
|
/** |
|
* Set the default action to fire if a key is typed. |
|
*/ |
|
public void setDefaultAction(Action a) { |
|
defaultAction = a; |
|
} |
|
public String getName() { |
|
return nm; |
|
} |
|
public Action getAction(KeyStroke key) { |
|
Action a = bindings.get(key); |
|
if ((a == null) && (parent != null)) { |
|
a = parent.getAction(key); |
|
} |
|
return a; |
|
} |
|
public KeyStroke[] getBoundKeyStrokes() { |
|
KeyStroke[] keys = new KeyStroke[bindings.size()]; |
|
int i = 0; |
|
for (Enumeration<KeyStroke> e = bindings.keys() ; e.hasMoreElements() ;) { |
|
keys[i++] = e.nextElement(); |
|
} |
|
return keys; |
|
} |
|
public Action[] getBoundActions() { |
|
Action[] actions = new Action[bindings.size()]; |
|
int i = 0; |
|
for (Enumeration<Action> e = bindings.elements() ; e.hasMoreElements() ;) { |
|
actions[i++] = e.nextElement(); |
|
} |
|
return actions; |
|
} |
|
public KeyStroke[] getKeyStrokesForAction(Action a) { |
|
if (a == null) { |
|
return null; |
|
} |
|
KeyStroke[] retValue = null; |
|
// Determine local bindings first. |
|
Vector<KeyStroke> keyStrokes = null; |
|
for (Enumeration<KeyStroke> keys = bindings.keys(); keys.hasMoreElements();) { |
|
KeyStroke key = keys.nextElement(); |
|
if (bindings.get(key) == a) { |
|
if (keyStrokes == null) { |
|
keyStrokes = new Vector<KeyStroke>(); |
|
} |
|
keyStrokes.addElement(key); |
|
} |
|
} |
|
// See if the parent has any. |
|
if (parent != null) { |
|
KeyStroke[] pStrokes = parent.getKeyStrokesForAction(a); |
|
if (pStrokes != null) { |
|
// Remove any bindings defined in the parent that |
|
// are locally defined. |
|
int rCount = 0; |
|
for (int counter = pStrokes.length - 1; counter >= 0; |
|
counter--) { |
|
if (isLocallyDefined(pStrokes[counter])) { |
|
pStrokes[counter] = null; |
|
rCount++; |
|
} |
|
} |
|
if (rCount > 0 && rCount < pStrokes.length) { |
|
if (keyStrokes == null) { |
|
keyStrokes = new Vector<KeyStroke>(); |
|
} |
|
for (int counter = pStrokes.length - 1; counter >= 0; |
|
counter--) { |
|
if (pStrokes[counter] != null) { |
|
keyStrokes.addElement(pStrokes[counter]); |
|
} |
|
} |
|
} |
|
else if (rCount == 0) { |
|
if (keyStrokes == null) { |
|
retValue = pStrokes; |
|
} |
|
else { |
|
retValue = new KeyStroke[keyStrokes.size() + |
|
pStrokes.length]; |
|
keyStrokes.copyInto(retValue); |
|
System.arraycopy(pStrokes, 0, retValue, |
|
keyStrokes.size(), pStrokes.length); |
|
keyStrokes = null; |
|
} |
|
} |
|
} |
|
} |
|
if (keyStrokes != null) { |
|
retValue = new KeyStroke[keyStrokes.size()]; |
|
keyStrokes.copyInto(retValue); |
|
} |
|
return retValue; |
|
} |
|
public boolean isLocallyDefined(KeyStroke key) { |
|
return bindings.containsKey(key); |
|
} |
|
public void addActionForKeyStroke(KeyStroke key, Action a) { |
|
bindings.put(key, a); |
|
} |
|
public void removeKeyStrokeBinding(KeyStroke key) { |
|
bindings.remove(key); |
|
} |
|
public void removeBindings() { |
|
bindings.clear(); |
|
} |
|
public Keymap getResolveParent() { |
|
return parent; |
|
} |
|
public void setResolveParent(Keymap parent) { |
|
this.parent = parent; |
|
} |
|
/** |
|
* String representation of the keymap... potentially |
|
* a very long string. |
|
*/ |
|
public String toString() { |
|
return "Keymap[" + nm + "]" + bindings; |
|
} |
|
String nm; |
|
Keymap parent; |
|
Hashtable<KeyStroke, Action> bindings; |
|
Action defaultAction; |
|
} |
|
/** |
|
* KeymapWrapper wraps a Keymap inside an InputMap. For KeymapWrapper |
|
* to be useful it must be used with a KeymapActionMap. |
|
* KeymapWrapper for the most part, is an InputMap with two parents. |
|
* The first parent visited is ALWAYS the Keymap, with the second |
|
* parent being the parent inherited from InputMap. If |
|
* <code>keymap.getAction</code> returns null, implying the Keymap |
|
* does not have a binding for the KeyStroke, |
|
* the parent is then visited. If the Keymap has a binding, the |
|
* Action is returned, if not and the KeyStroke represents a |
|
* KeyTyped event and the Keymap has a defaultAction, |
|
* <code>DefaultActionKey</code> is returned. |
|
* <p>KeymapActionMap is then able to transate the object passed in |
|
* to either message the Keymap, or message its default implementation. |
|
*/ |
|
static class KeymapWrapper extends InputMap { |
|
static final Object DefaultActionKey = new Object(); |
|
private Keymap keymap; |
|
KeymapWrapper(Keymap keymap) { |
|
this.keymap = keymap; |
|
} |
|
public KeyStroke[] keys() { |
|
KeyStroke[] sKeys = super.keys(); |
|
KeyStroke[] keymapKeys = keymap.getBoundKeyStrokes(); |
|
int sCount = (sKeys == null) ? 0 : sKeys.length; |
|
int keymapCount = (keymapKeys == null) ? 0 : keymapKeys.length; |
|
if (sCount == 0) { |
|
return keymapKeys; |
|
} |
|
if (keymapCount == 0) { |
|
return sKeys; |
|
} |
|
KeyStroke[] retValue = new KeyStroke[sCount + keymapCount]; |
|
// There may be some duplication here... |
|
System.arraycopy(sKeys, 0, retValue, 0, sCount); |
|
System.arraycopy(keymapKeys, 0, retValue, sCount, keymapCount); |
|
return retValue; |
|
} |
|
public int size() { |
|
// There may be some duplication here... |
|
KeyStroke[] keymapStrokes = keymap.getBoundKeyStrokes(); |
|
int keymapCount = (keymapStrokes == null) ? 0: |
|
keymapStrokes.length; |
|
return super.size() + keymapCount; |
|
} |
|
public Object get(KeyStroke keyStroke) { |
|
Object retValue = keymap.getAction(keyStroke); |
|
if (retValue == null) { |
|
retValue = super.get(keyStroke); |
|
if (retValue == null && |
|
keyStroke.getKeyChar() != KeyEvent.CHAR_UNDEFINED && |
|
keymap.getDefaultAction() != null) { |
|
// Implies this is a KeyTyped event, use the default |
|
// action. |
|
retValue = DefaultActionKey; |
|
} |
|
} |
|
return retValue; |
|
} |
|
} |
|
/** |
|
* Wraps a Keymap inside an ActionMap. This is used with |
|
* a KeymapWrapper. If <code>get</code> is passed in |
|
* <code>KeymapWrapper.DefaultActionKey</code>, the default action is |
|
* returned, otherwise if the key is an Action, it is returned. |
|
*/ |
|
static class KeymapActionMap extends ActionMap { |
|
private Keymap keymap; |
|
KeymapActionMap(Keymap keymap) { |
|
this.keymap = keymap; |
|
} |
|
public Object[] keys() { |
|
Object[] sKeys = super.keys(); |
|
Object[] keymapKeys = keymap.getBoundActions(); |
|
int sCount = (sKeys == null) ? 0 : sKeys.length; |
|
int keymapCount = (keymapKeys == null) ? 0 : keymapKeys.length; |
|
boolean hasDefault = (keymap.getDefaultAction() != null); |
|
if (hasDefault) { |
|
keymapCount++; |
|
} |
|
if (sCount == 0) { |
|
if (hasDefault) { |
|
Object[] retValue = new Object[keymapCount]; |
|
if (keymapCount > 1) { |
|
System.arraycopy(keymapKeys, 0, retValue, 0, |
|
keymapCount - 1); |
|
} |
|
retValue[keymapCount - 1] = KeymapWrapper.DefaultActionKey; |
|
return retValue; |
|
} |
|
return keymapKeys; |
|
} |
|
if (keymapCount == 0) { |
|
return sKeys; |
|
} |
|
Object[] retValue = new Object[sCount + keymapCount]; |
|
// There may be some duplication here... |
|
System.arraycopy(sKeys, 0, retValue, 0, sCount); |
|
if (hasDefault) { |
|
if (keymapCount > 1) { |
|
System.arraycopy(keymapKeys, 0, retValue, sCount, |
|
keymapCount - 1); |
|
} |
|
retValue[sCount + keymapCount - 1] = KeymapWrapper. |
|
DefaultActionKey; |
|
} |
|
else { |
|
System.arraycopy(keymapKeys, 0, retValue, sCount, keymapCount); |
|
} |
|
return retValue; |
|
} |
|
public int size() { |
|
// There may be some duplication here... |
|
Object[] actions = keymap.getBoundActions(); |
|
int keymapCount = (actions == null) ? 0 : actions.length; |
|
if (keymap.getDefaultAction() != null) { |
|
keymapCount++; |
|
} |
|
return super.size() + keymapCount; |
|
} |
|
public Action get(Object key) { |
|
Action retValue = super.get(key); |
|
if (retValue == null) { |
|
// Try the Keymap. |
|
if (key == KeymapWrapper.DefaultActionKey) { |
|
retValue = keymap.getDefaultAction(); |
|
} |
|
else if (key instanceof Action) { |
|
// This is a little iffy, technically an Action is |
|
// a valid Key. We're assuming the Action came from |
|
// the InputMap though. |
|
retValue = (Action)key; |
|
} |
|
} |
|
return retValue; |
|
} |
|
} |
|
private static final Object FOCUSED_COMPONENT = |
|
new StringBuilder("JTextComponent_FocusedComponent"); |
|
/** |
|
* The default keymap that will be shared by all |
|
* <code>JTextComponent</code> instances unless they |
|
* have had a different keymap set. |
|
*/ |
|
public static final String DEFAULT_KEYMAP = "default"; |
|
/** |
|
* Event to use when firing a notification of change to caret |
|
* position. This is mutable so that the event can be reused |
|
* since caret events can be fairly high in bandwidth. |
|
*/ |
|
static class MutableCaretEvent extends CaretEvent implements ChangeListener, FocusListener, MouseListener { |
|
MutableCaretEvent(JTextComponent c) { |
|
super(c); |
|
} |
|
final void fire() { |
|
JTextComponent c = (JTextComponent) getSource(); |
|
if (c != null) { |
|
Caret caret = c.getCaret(); |
|
dot = caret.getDot(); |
|
mark = caret.getMark(); |
|
c.fireCaretUpdate(this); |
|
} |
|
} |
|
public final String toString() { |
|
return "dot=" + dot + "," + "mark=" + mark; |
|
} |
|
// --- CaretEvent methods ----------------------- |
|
public final int getDot() { |
|
return dot; |
|
} |
|
public final int getMark() { |
|
return mark; |
|
} |
|
// --- ChangeListener methods ------------------- |
|
public final void stateChanged(ChangeEvent e) { |
|
if (! dragActive) { |
|
fire(); |
|
} |
|
} |
|
// --- FocusListener methods ----------------------------------- |
|
public void focusGained(FocusEvent fe) { |
|
AppContext.getAppContext().put(FOCUSED_COMPONENT, |
|
fe.getSource()); |
|
} |
|
public void focusLost(FocusEvent fe) { |
|
} |
|
// --- MouseListener methods ----------------------------------- |
|
/** |
|
* Requests focus on the associated |
|
* text component, and try to set the cursor position. |
|
* |
|
* @param e the mouse event |
|
* @see MouseListener#mousePressed |
|
*/ |
|
public final void mousePressed(MouseEvent e) { |
|
dragActive = true; |
|
} |
|
/** |
|
* Called when the mouse is released. |
|
* |
|
* @param e the mouse event |
|
* @see MouseListener#mouseReleased |
|
*/ |
|
public final void mouseReleased(MouseEvent e) { |
|
dragActive = false; |
|
fire(); |
|
} |
|
public final void mouseClicked(MouseEvent e) { |
|
} |
|
public final void mouseEntered(MouseEvent e) { |
|
} |
|
public final void mouseExited(MouseEvent e) { |
|
} |
|
private boolean dragActive; |
|
private int dot; |
|
private int mark; |
|
} |
|
// |
|
// Process any input method events that the component itself |
|
// recognizes. The default on-the-spot handling for input method |
|
// composed(uncommitted) text is done here after all input |
|
// method listeners get called for stealing the events. |
|
// |
|
protected void processInputMethodEvent(InputMethodEvent e) { |
|
// let listeners handle the events |
|
super.processInputMethodEvent(e); |
|
if (!e.isConsumed()) { |
|
if (! isEditable()) { |
|
return; |
|
} else { |
|
switch (e.getID()) { |
|
case InputMethodEvent.INPUT_METHOD_TEXT_CHANGED: |
|
replaceInputMethodText(e); |
|
// fall through |
|
case InputMethodEvent.CARET_POSITION_CHANGED: |
|
setInputMethodCaretPosition(e); |
|
break; |
|
} |
|
} |
|
e.consume(); |
|
} |
|
} |
|
// |
|
// Overrides this method to become an active input method client. |
|
// |
|
public InputMethodRequests getInputMethodRequests() { |
|
if (inputMethodRequestsHandler == null) { |
|
inputMethodRequestsHandler = new InputMethodRequestsHandler(); |
|
Document doc = getDocument(); |
|
if (doc != null) { |
|
doc.addDocumentListener((DocumentListener)inputMethodRequestsHandler); |
|
} |
|
} |
|
return inputMethodRequestsHandler; |
|
} |
|
// |
|
// Overrides this method to watch the listener installed. |
|
// |
|
public void addInputMethodListener(InputMethodListener l) { |
|
super.addInputMethodListener(l); |
|
if (l != null) { |
|
needToSendKeyTypedEvent = false; |
|
checkedInputOverride = true; |
|
} |
|
} |
|
// |
|
// Default implementation of the InputMethodRequests interface. |
|
// |
|
class InputMethodRequestsHandler implements InputMethodRequests, DocumentListener { |
|
// --- InputMethodRequests methods --- |
|
public AttributedCharacterIterator cancelLatestCommittedText( |
|
Attribute[] attributes) { |
|
Document doc = getDocument(); |
|
if ((doc != null) && (latestCommittedTextStart != null) |
|
&& (!latestCommittedTextStart.equals(latestCommittedTextEnd))) { |
|
try { |
|
int startIndex = latestCommittedTextStart.getOffset(); |
|
int endIndex = latestCommittedTextEnd.getOffset(); |
|
String latestCommittedText = |
|
doc.getText(startIndex, endIndex - startIndex); |
|
doc.remove(startIndex, endIndex - startIndex); |
|
return new AttributedString(latestCommittedText).getIterator(); |
|
} catch (BadLocationException ble) {} |
|
} |
|
return null; |
|
} |
|
public AttributedCharacterIterator getCommittedText(int beginIndex, |
|
int endIndex, Attribute[] attributes) { |
|
int composedStartIndex = 0; |
|
int composedEndIndex = 0; |
|
if (composedTextExists()) { |
|
composedStartIndex = composedTextStart.getOffset(); |
|
composedEndIndex = composedTextEnd.getOffset(); |
|
} |
|
String committed; |
|
try { |
|
if (beginIndex < composedStartIndex) { |
|
if (endIndex <= composedStartIndex) { |
|
committed = getText(beginIndex, endIndex - beginIndex); |
|
} else { |
|
int firstPartLength = composedStartIndex - beginIndex; |
|
committed = getText(beginIndex, firstPartLength) + |
|
getText(composedEndIndex, endIndex - beginIndex - firstPartLength); |
|
} |
|
} else { |
|
committed = getText(beginIndex + (composedEndIndex - composedStartIndex), |
|
endIndex - beginIndex); |
|
} |
|
} catch (BadLocationException ble) { |
|
throw new IllegalArgumentException("Invalid range"); |
|
} |
|
return new AttributedString(committed).getIterator(); |
|
} |
|
public int getCommittedTextLength() { |
|
Document doc = getDocument(); |
|
int length = 0; |
|
if (doc != null) { |
|
length = doc.getLength(); |
|
if (composedTextContent != null) { |
|
if (composedTextEnd == null |
|
|| composedTextStart == null) { |
|
/* |
|
* fix for : 6355666 |
|
* this is the case when this method is invoked |
|
* from DocumentListener. At this point |
|
* composedTextEnd and composedTextStart are |
|
* not defined yet. |
|
*/ |
|
length -= composedTextContent.length(); |
|
} else { |
|
length -= composedTextEnd.getOffset() - |
|
composedTextStart.getOffset(); |
|
} |
|
} |
|
} |
|
return length; |
|
} |
|
public int getInsertPositionOffset() { |
|
int composedStartIndex = 0; |
|
int composedEndIndex = 0; |
|
if (composedTextExists()) { |
|
composedStartIndex = composedTextStart.getOffset(); |
|
composedEndIndex = composedTextEnd.getOffset(); |
|
} |
|
int caretIndex = getCaretPosition(); |
|
if (caretIndex < composedStartIndex) { |
|
return caretIndex; |
|
} else if (caretIndex < composedEndIndex) { |
|
return composedStartIndex; |
|
} else { |
|
return caretIndex - (composedEndIndex - composedStartIndex); |
|
} |
|
} |
|
public TextHitInfo getLocationOffset(int x, int y) { |
|
if (composedTextAttribute == null) { |
|
return null; |
|
} else { |
|
Point p = getLocationOnScreen(); |
|
p.x = x - p.x; |
|
p.y = y - p.y; |
|
int pos = viewToModel(p); |
|
if ((pos >= composedTextStart.getOffset()) && |
|
(pos <= composedTextEnd.getOffset())) { |
|
return TextHitInfo.leading(pos - composedTextStart.getOffset()); |
|
} else { |
|
return null; |
|
} |
|
} |
|
} |
|
public Rectangle getTextLocation(TextHitInfo offset) { |
|
Rectangle r; |
|
try { |
|
r = modelToView(getCaretPosition()); |
|
if (r != null) { |
|
Point p = getLocationOnScreen(); |
|
r.translate(p.x, p.y); |
|
} |
|
} catch (BadLocationException ble) { |
|
r = null; |
|
} |
|
if (r == null) |
|
r = new Rectangle(); |
|
return r; |
|
} |
|
public AttributedCharacterIterator getSelectedText( |
|
Attribute[] attributes) { |
|
String selection = JTextComponent.this.getSelectedText(); |
|
if (selection != null) { |
|
return new AttributedString(selection).getIterator(); |
|
} else { |
|
return null; |
|
} |
|
} |
|
// --- DocumentListener methods --- |
|
public void changedUpdate(DocumentEvent e) { |
|
latestCommittedTextStart = latestCommittedTextEnd = null; |
|
} |
|
public void insertUpdate(DocumentEvent e) { |
|
latestCommittedTextStart = latestCommittedTextEnd = null; |
|
} |
|
public void removeUpdate(DocumentEvent e) { |
|
latestCommittedTextStart = latestCommittedTextEnd = null; |
|
} |
|
} |
|
// |
|
// Replaces the current input method (composed) text according to |
|
// the passed input method event. This method also inserts the |
|
// committed text into the document. |
|
// |
|
private void replaceInputMethodText(InputMethodEvent e) { |
|
int commitCount = e.getCommittedCharacterCount(); |
|
AttributedCharacterIterator text = e.getText(); |
|
int composedTextIndex; |
|
// old composed text deletion |
|
Document doc = getDocument(); |
|
if (composedTextExists()) { |
|
try { |
|
doc.remove(composedTextStart.getOffset(), |
|
composedTextEnd.getOffset() - |
|
composedTextStart.getOffset()); |
|
} catch (BadLocationException ble) {} |
|
composedTextStart = composedTextEnd = null; |
|
composedTextAttribute = null; |
|
composedTextContent = null; |
|
} |
|
if (text != null) { |
|
text.first(); |
|
int committedTextStartIndex = 0; |
|
int committedTextEndIndex = 0; |
|
// committed text insertion |
|
if (commitCount > 0) { |
|
// Remember latest committed text start index |
|
committedTextStartIndex = caret.getDot(); |
|
// Need to generate KeyTyped events for the committed text for components |
|
// that are not aware they are active input method clients. |
|
if (shouldSynthensizeKeyEvents()) { |
|
for (char c = text.current(); commitCount > 0; |
|
c = text.next(), commitCount--) { |
|
KeyEvent ke = new KeyEvent(this, KeyEvent.KEY_TYPED, |
|
EventQueue.getMostRecentEventTime(), |
|
0, KeyEvent.VK_UNDEFINED, c); |
|
processKeyEvent(ke); |
|
} |
|
} else { |
|
StringBuilder strBuf = new StringBuilder(); |
|
for (char c = text.current(); commitCount > 0; |
|
c = text.next(), commitCount--) { |
|
strBuf.append(c); |
|
} |
|
// map it to an ActionEvent |
|
mapCommittedTextToAction(strBuf.toString()); |
|
} |
|
// Remember latest committed text end index |
|
committedTextEndIndex = caret.getDot(); |
|
} |
|
// new composed text insertion |
|
composedTextIndex = text.getIndex(); |
|
if (composedTextIndex < text.getEndIndex()) { |
|
createComposedTextAttribute(composedTextIndex, text); |
|
try { |
|
replaceSelection(null); |
|
doc.insertString(caret.getDot(), composedTextContent, |
|
composedTextAttribute); |
|
composedTextStart = doc.createPosition(caret.getDot() - |
|
composedTextContent.length()); |
|
composedTextEnd = doc.createPosition(caret.getDot()); |
|
} catch (BadLocationException ble) { |
|
composedTextStart = composedTextEnd = null; |
|
composedTextAttribute = null; |
|
composedTextContent = null; |
|
} |
|
} |
|
// Save the latest committed text information |
|
if (committedTextStartIndex != committedTextEndIndex) { |
|
try { |
|
latestCommittedTextStart = doc. |
|
createPosition(committedTextStartIndex); |
|
latestCommittedTextEnd = doc. |
|
createPosition(committedTextEndIndex); |
|
} catch (BadLocationException ble) { |
|
latestCommittedTextStart = |
|
latestCommittedTextEnd = null; |
|
} |
|
} else { |
|
latestCommittedTextStart = |
|
latestCommittedTextEnd = null; |
|
} |
|
} |
|
} |
|
private void createComposedTextAttribute(int composedIndex, |
|
AttributedCharacterIterator text) { |
|
Document doc = getDocument(); |
|
StringBuilder strBuf = new StringBuilder(); |
|
// create attributed string with no attributes |
|
for (char c = text.setIndex(composedIndex); |
|
c != CharacterIterator.DONE; c = text.next()) { |
|
strBuf.append(c); |
|
} |
|
composedTextContent = strBuf.toString(); |
|
composedTextAttribute = new SimpleAttributeSet(); |
|
composedTextAttribute.addAttribute(StyleConstants.ComposedTextAttribute, |
|
new AttributedString(text, composedIndex, text.getEndIndex())); |
|
} |
|
/** |
|
* Saves composed text around the specified position. |
|
* |
|
* The composed text (if any) around the specified position is saved |
|
* in a backing store and removed from the document. |
|
* |
|
* @param pos document position to identify the composed text location |
|
* @return {@code true} if the composed text exists and is saved, |
|
* {@code false} otherwise |
|
* @see #restoreComposedText |
|
* @since 1.7 |
|
*/ |
|
protected boolean saveComposedText(int pos) { |
|
if (composedTextExists()) { |
|
int start = composedTextStart.getOffset(); |
|
int len = composedTextEnd.getOffset() - |
|
composedTextStart.getOffset(); |
|
if (pos >= start && pos <= start + len) { |
|
try { |
|
getDocument().remove(start, len); |
|
return true; |
|
} catch (BadLocationException ble) {} |
|
} |
|
} |
|
return false; |
|
} |
|
/** |
|
* Restores composed text previously saved by {@code saveComposedText}. |
|
* |
|
* The saved composed text is inserted back into the document. This method |
|
* should be invoked only if {@code saveComposedText} returns {@code true}. |
|
* |
|
* @see #saveComposedText |
|
* @since 1.7 |
|
*/ |
|
protected void restoreComposedText() { |
|
Document doc = getDocument(); |
|
try { |
|
doc.insertString(caret.getDot(), |
|
composedTextContent, |
|
composedTextAttribute); |
|
composedTextStart = doc.createPosition(caret.getDot() - |
|
composedTextContent.length()); |
|
composedTextEnd = doc.createPosition(caret.getDot()); |
|
} catch (BadLocationException ble) {} |
|
} |
|
// |
|
// Map committed text to an ActionEvent. If the committed text length is 1, |
|
// treat it as a KeyStroke, otherwise or there is no KeyStroke defined, |
|
// treat it just as a default action. |
|
// |
|
private void mapCommittedTextToAction(String committedText) { |
|
Keymap binding = getKeymap(); |
|
if (binding != null) { |
|
Action a = null; |
|
if (committedText.length() == 1) { |
|
KeyStroke k = KeyStroke.getKeyStroke(committedText.charAt(0)); |
|
a = binding.getAction(k); |
|
} |
|
if (a == null) { |
|
a = binding.getDefaultAction(); |
|
} |
|
if (a != null) { |
|
ActionEvent ae = |
|
new ActionEvent(this, ActionEvent.ACTION_PERFORMED, |
|
committedText, |
|
EventQueue.getMostRecentEventTime(), |
|
getCurrentEventModifiers()); |
|
a.actionPerformed(ae); |
|
} |
|
} |
|
} |
|
// |
|
// Sets the caret position according to the passed input method |
|
// event. Also, sets/resets composed text caret appropriately. |
|
// |
|
private void setInputMethodCaretPosition(InputMethodEvent e) { |
|
int dot; |
|
if (composedTextExists()) { |
|
dot = composedTextStart.getOffset(); |
|
if (!(caret instanceof ComposedTextCaret)) { |
|
if (composedTextCaret == null) { |
|
composedTextCaret = new ComposedTextCaret(); |
|
} |
|
originalCaret = caret; |
|
// Sets composed text caret |
|
exchangeCaret(originalCaret, composedTextCaret); |
|
} |
|
TextHitInfo caretPos = e.getCaret(); |
|
if (caretPos != null) { |
|
int index = caretPos.getInsertionIndex(); |
|
dot += index; |
|
if (index == 0) { |
|
// Scroll the component if needed so that the composed text |
|
// becomes visible. |
|
try { |
|
Rectangle d = modelToView(dot); |
|
Rectangle end = modelToView(composedTextEnd.getOffset()); |
|
Rectangle b = getBounds(); |
|
d.x += Math.min(end.x - d.x, b.width); |
|
scrollRectToVisible(d); |
|
} catch (BadLocationException ble) {} |
|
} |
|
} |
|
caret.setDot(dot); |
|
} else if (caret instanceof ComposedTextCaret) { |
|
dot = caret.getDot(); |
|
// Restores original caret |
|
exchangeCaret(caret, originalCaret); |
|
caret.setDot(dot); |
|
} |
|
} |
|
private void exchangeCaret(Caret oldCaret, Caret newCaret) { |
|
int blinkRate = oldCaret.getBlinkRate(); |
|
setCaret(newCaret); |
|
caret.setBlinkRate(blinkRate); |
|
caret.setVisible(hasFocus()); |
|
} |
|
/** |
|
* Returns true if KeyEvents should be synthesized from an InputEvent. |
|
*/ |
|
private boolean shouldSynthensizeKeyEvents() { |
|
if (!checkedInputOverride) { |
|
// Checks whether the client code overrides processInputMethodEvent. |
|
// If it is overridden, need not to generate KeyTyped events for committed text. |
|
// If it's not, behave as an passive input method client. |
|
needToSendKeyTypedEvent = !METHOD_OVERRIDDEN.get(getClass()); |
|
checkedInputOverride = true; |
|
} |
|
return needToSendKeyTypedEvent; |
|
} |
|
// |
|
// Checks whether a composed text in this text component |
|
// |
|
boolean composedTextExists() { |
|
return (composedTextStart != null); |
|
} |
|
// |
|
// Caret implementation for editing the composed text. |
|
// |
|
class ComposedTextCaret extends DefaultCaret implements Serializable { |
|
Color bg; |
|
// |
|
// Get the background color of the component |
|
// |
|
public void install(JTextComponent c) { |
|
super.install(c); |
|
Document doc = c.getDocument(); |
|
if (doc instanceof StyledDocument) { |
|
StyledDocument sDoc = (StyledDocument)doc; |
|
Element elem = sDoc.getCharacterElement(c.composedTextStart.getOffset()); |
|
AttributeSet attr = elem.getAttributes(); |
|
bg = sDoc.getBackground(attr); |
|
} |
|
if (bg == null) { |
|
bg = c.getBackground(); |
|
} |
|
} |
|
// |
|
// Draw caret in XOR mode. |
|
// |
|
public void paint(Graphics g) { |
|
if(isVisible()) { |
|
try { |
|
Rectangle r = component.modelToView(getDot()); |
|
g.setXORMode(bg); |
|
g.drawLine(r.x, r.y, r.x, r.y + r.height - 1); |
|
g.setPaintMode(); |
|
} catch (BadLocationException e) { |
|
// can't render I guess |
|
//System.err.println("Can't render cursor"); |
|
} |
|
} |
|
} |
|
// |
|
// If some area other than the composed text is clicked by mouse, |
|
// issue endComposition() to force commit the composed text. |
|
// |
|
protected void positionCaret(MouseEvent me) { |
|
JTextComponent host = component; |
|
Point pt = new Point(me.getX(), me.getY()); |
|
int offset = host.viewToModel(pt); |
|
int composedStartIndex = host.composedTextStart.getOffset(); |
|
if ((offset < composedStartIndex) || |
|
(offset > composedTextEnd.getOffset())) { |
|
try { |
|
// Issue endComposition |
|
Position newPos = host.getDocument().createPosition(offset); |
|
host.getInputContext().endComposition(); |
|
// Post a caret positioning runnable to assure that the positioning |
|
// occurs *after* committing the composed text. |
|
EventQueue.invokeLater(new DoSetCaretPosition(host, newPos)); |
|
} catch (BadLocationException ble) { |
|
System.err.println(ble); |
|
} |
|
} else { |
|
// Normal processing |
|
super.positionCaret(me); |
|
} |
|
} |
|
} |
|
// |
|
// Runnable class for invokeLater() to set caret position later. |
|
// |
|
private class DoSetCaretPosition implements Runnable { |
|
JTextComponent host; |
|
Position newPos; |
|
DoSetCaretPosition(JTextComponent host, Position newPos) { |
|
this.host = host; |
|
this.newPos = newPos; |
|
} |
|
public void run() { |
|
host.setCaretPosition(newPos.getOffset()); |
|
} |
|
} |
|
} |