Java tutorial
/* * Copyright (c) 1997, 2018, 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.JavaBean; import java.beans.BeanProperty; 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.geom.Point2D; import java.awt.geom.Rectangle2D; 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>Caret Changes</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>Commands</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>Text Input</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 {@extLink imf_overview Input Method Framework} * 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 class="striped"> * <caption>Stages of keyboard and input method event handling</caption> * <thead> * <tr> * <th scope="col">Stage * <th scope="col">KeyEvent * <th scope="col">InputMethodEvent * </thead> * <tbody> * <tr> * <th scope="row">1. * <td>input methods * <td>(generated here) * <tr> * <th scope="row">2. * <td>focus manager * <td> * </tr> * <tr> * <th scope="row">3. * <td>registered key listeners * <td>registered input method listeners * <tr> * <th scope="row">4. * <td> * <td>input method handling in JTextComponent * <tr> * <th scope="row">5. * <td colspan=2>keymap handling using the current keymap * <tr> * <th scope="row">6. * <td>keyboard handling in JComponent (e.g. accelerators, component * navigation, etc.) * <td> * </tbody> * </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>Model/View Split</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>Location Information</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>Undo/Redo support</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>Thread Safety</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>Newlines</b> * <dd> * For a discussion on how newlines are handled, see * <a href="DefaultEditorKit.html">DefaultEditorKit</a>. * * * <dt><b>Printing support</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}. * * @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 */ @JavaBean(defaultProperty = "UI") @SwingContainer(false) @SuppressWarnings("serial") // Same-version serialization only 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 */ @BeanProperty(bound = false) 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 */ @BeanProperty(expert = true, description = "the text document model") 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 */ @BeanProperty(bound = false) 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 */ @BeanProperty(description = "desired space between the border and text area") 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. * @param filter the filter * * @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 */ @BeanProperty(expert = true, description = "the caret used to select/navigate") 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 */ @BeanProperty(expert = true, description = "object responsible for background highlights") 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 */ @BeanProperty(description = "set of key event to action bindings to use") 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 */ @BeanProperty(bound = false, description = "determines whether automatic drag handling is enabled") public void setDragEnabled(boolean b) { checkDragEnabled(b); dragEnabled = b; } private static void checkDragEnabled(boolean b) { if (b && GraphicsEnvironment.isHeadless()) { throw new HeadlessException(); } } /** * 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) { checkDropMode(dropMode); this.dropMode = dropMode; } private static void checkDropMode(DropMode dropMode) { if (dropMode != null) { switch (dropMode) { case USE_SELECTION: case INSERT: 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> */ @SuppressWarnings("deprecation") 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 */ @BeanProperty(bound = false) 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(); @SuppressWarnings("unchecked") 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}. */ @SuppressWarnings("serial") // Same-version serialization only 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 */ @BeanProperty(preferred = true, description = "the color used to render the caret") 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 */ @BeanProperty(preferred = true, description = "color used to render selection background") 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 */ @BeanProperty(preferred = true, description = "color used to render selected text") 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 */ @BeanProperty(preferred = true, description = "color used to render disabled text") 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 * * @deprecated replaced by * {@link #modelToView2D(int)} */ @Deprecated(since = "9") public Rectangle modelToView(int pos) throws BadLocationException { return getUI().modelToView(this, pos); } /** * 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 {@code >= 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#modelToView2D * * @since 9 */ public Rectangle2D modelToView2D(int pos) throws BadLocationException { return getUI().modelToView2D(this, pos, Position.Bias.Forward); } /** * 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 * * @deprecated replaced by * {@link #viewToModel2D(Point2D)} */ @Deprecated(since = "9") public int viewToModel(Point pt) { return getUI().viewToModel(this, pt); } /** * 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 {@code >= 0} from the start of the document, * or {@code -1} if the component does not yet have a positive * size. * @see TextUI#viewToModel2D * * @since 9 */ public int viewToModel2D(Point2D pt) { return getUI().viewToModel2D(this, pt, new Position.Bias[1]); } /** * 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 */ @BeanProperty(description = "accelerator character used to grab focus") 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 */ @BeanProperty(bound = false, 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 */ @BeanProperty(bound = false, 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 */ @BeanProperty(bound = false) 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 */ @BeanProperty(description = "specifies if the text can be edited") 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 */ @BeanProperty(bound = false, 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 */ @BeanProperty(bound = false, 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 */ @SuppressWarnings("deprecation") 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> */ @BeanProperty(bound = false) 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 */ @BeanProperty(bound = false) 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 */ @BeanProperty(bound = false) 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(null, runnablePrinting, "JTextComponentPrint", 0, false).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> */ @BeanProperty(bound = false) 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}. */ @SuppressWarnings("serial") // Same-version serialization only 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, caretPos, 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 = 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 = 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 = 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 { ObjectInputStream.GetField f = s.readFields(); model = (Document) f.get("model", null); navigationFilter = (NavigationFilter) f.get("navigationFilter", null); caretColor = (Color) f.get("caretColor", null); selectionColor = (Color) f.get("selectionColor", null); selectedTextColor = (Color) f.get("selectedTextColor", null); disabledTextColor = (Color) f.get("disabledTextColor", null); editable = f.get("editable", false); margin = (Insets) f.get("margin", null); focusAccelerator = f.get("focusAccelerator", '\0'); boolean newDragEnabled = f.get("dragEnabled", false); checkDragEnabled(newDragEnabled); dragEnabled = newDragEnabled; DropMode newDropMode = (DropMode) f.get("dropMode", DropMode.USE_SELECTION); checkDropMode(newDropMode); dropMode = newDropMode; composedTextAttribute = (SimpleAttributeSet) f.get("composedTextAttribute", null); composedTextContent = (String) f.get("composedTextContent", null); composedTextStart = (Position) f.get("composedTextStart", null); composedTextEnd = (Position) f.get("composedTextEnd", null); latestCommittedTextStart = (Position) f.get("latestCommittedTextStart", null); latestCommittedTextEnd = (Position) f.get("latestCommittedTextEnd", null); composedTextCaret = (ComposedTextCaret) f.get("composedTextCaret", null); checkedInputOverride = f.get("checkedInputOverride", false); needToSendKeyTypedEvent = f.get("needToSendKeyTypedEvent", false); 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); } @SuppressWarnings("deprecation") 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. // @SuppressWarnings("fallthrough") 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. // @BeanProperty(bound = false) 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()); } } }