/* * Copyright (c) 1997, 2010, 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 java.lang.reflect.Method; import java.security.AccessController; import java.security.PrivilegedAction; import java.beans.Transient; import java.util.Collections; import java.util.HashMap; import java.util.Hashtable; import java.util.Enumeration; import java.util.Vector; import java.util.Map; import java.util.concurrent.*; import java.io.*; import java.awt.*; import java.awt.event.*; import java.awt.print.*; import java.awt.datatransfer.*; import java.awt.im.InputContext; import java.awt.im.InputMethodRequests; import java.awt.font.TextHitInfo; import java.awt.font.TextAttribute; import java.awt.print.Printable; import java.awt.print.PrinterException; import javax.print.PrintService; import javax.print.attribute.PrintRequestAttributeSet; import java.text.*; import java.text.AttributedCharacterIterator.Attribute; import javax.swing.*; import javax.swing.event.*; import javax.swing.plaf.*; import javax.accessibility.*; import javax.print.attribute.*; import sun.awt.AppContext; import sun.swing.PrintingStatus; import sun.swing.SwingUtilities2; import sun.swing.text.TextComponentPrintable; import sun.swing.SwingAccessor; /** * JTextComponent is the base class for swing text * components. It tries to be compatible with the * java.awt.TextComponent 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 * General Rules for Using Text Components, * a section in The Java Tutorial. * *

*

*
Caret Changes *
* 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 CaretListener * interface that have been registered with the text component. * The UI will install a default caret unless a customized caret * has been set.
* 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. DefaultCaret * tries to make itself visible which may lead to scrolling * of a text component within JScrollPane. The default caret * behavior can be changed by the {@link DefaultCaret#setUpdatePolicy} method. *
* Note: Non-editable text components also have a caret though * it may not be painted. * *

*

Commands *
* 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 Action interface, * using the TextAction 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. * *

*

Text Input *
* The text components support flexible and internationalized text input, using * keymaps and the input method framework, while maintaining compatibility with * the AWT listener model. *

* 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 TextAction. * TextAction can determine which JTextComponent * most recently has or had focus and therefore is the subject of * the action (In the case that the ActionEvent * sent to the action doesn't contain the target text component as its source). *

* The 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. * JTextComponent is an active client 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 * keyTyped key events and committed text in input method events. *

* 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. *

* Keyboard event and input method events are handled in the following stages, * with each stage capable of consuming the event: * * * * * * * * * * * * * * * * * * * * * * * * * *

Stage

KeyEvent

InputMethodEvent

1. input methods (generated here)
2. focus manager
3. registered key listenersregistered input method listeners
4. input method handling in JTextComponent
5. keymap handling using the current keymap
6. keyboard handling in JComponent (e.g. accelerators, component navigation, etc.)
* *

* 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. *

* By default the component will create a keymap (named DEFAULT_KEYMAP) * 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: *

* *

*

Model/View Split *
* 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). * *

Diagram showing interaction between Controller, Document, events, and ViewFactory

* *

* 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. * *

*

Location Information *
* 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. * *

*

Undo/Redo support *
* 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 UndoableEdit 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. * *

*

Thread Safety *
* 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 AbstractDocument * describes the assumptions of the protection provided. * The methods that are safe to call asynchronously are marked * with comments. * *

*

Newlines *
* For a discussion on how newlines are handled, see * DefaultEditorKit. * *

*

Printing support *
* Several {@link #print print} methods are provided for basic * document printing. If more advanced printing is needed, use the * {@link #getPrintable} method. *
* *

* Warning: * 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 JavaBeansTM * has been added to the java.beans package. * Please see {@link java.beans.XMLEncoder}. * * @beaninfo * attribute: isContainer false * * @author Timothy Prinzing * @author Igor Kushnirskiy (printing support) * @see Document * @see DocumentEvent * @see DocumentListener * @see Caret * @see CaretEvent * @see CaretListener * @see TextUI * @see View * @see ViewFactory */ public abstract class JTextComponent extends JComponent implements Scrollable, Accessible { /** * Creates a new JTextComponent. * 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 null. */ 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 getUIClassID(). The type of * the UI is TextUI. invalidate * 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 CaretListeners * or an empty * array if no caret listeners are currently registered * * @see #addCaretListener * @see #removeCaretListener * * @since 1.4 */ public CaretListener[] getCaretListeners() { return listenerList.getListeners(CaretListener.class); } /** * Notifies all listeners that have registered interest for * notification on this event type. The event instance * is lazily created using the parameters passed into * the fire method. The listener list is processed in a * last-to-first manner. * * @param e the event * @see EventListenerList */ protected void fireCaretUpdate(CaretEvent e) { // Guaranteed to return a non-null array Object[] listeners = listenerList.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length-2; i>=0; i-=2) { if (listeners[i]==CaretListener.class) { ((CaretListener)listeners[i+1]).caretUpdate(e); } } } /** * Associates the editor with a text document. * The currently registered factory is used to build a view for * the document, which gets displayed by the editor after revalidation. * A PropertyChange event ("document") is propagated to each listener. * * @param doc the document to display/edit * @see #getDocument * @beaninfo * description: the text document model * bound: true * expert: true */ public void setDocument(Document doc) { Document old = model; /* * aquire a read lock on the old model to prevent notification of * mutations while we disconnecting the old model. */ try { if (old instanceof AbstractDocument) { ((AbstractDocument)old).readLock(); } if (accessibleContext != null) { model.removeDocumentListener( ((AccessibleJTextComponent)accessibleContext)); } if (inputMethodRequestsHandler != null) { model.removeDocumentListener((DocumentListener)inputMethodRequestsHandler); } model = doc; // Set the document's run direction property to match the // component's ComponentOrientation property. Boolean runDir = getComponentOrientation().isLeftToRight() ? TextAttribute.RUN_DIRECTION_LTR : TextAttribute.RUN_DIRECTION_RTL; if (runDir != doc.getProperty(TextAttribute.RUN_DIRECTION)) { doc.putProperty(TextAttribute.RUN_DIRECTION, runDir ); } firePropertyChange("document", old, doc); } finally { if (old instanceof AbstractDocument) { ((AbstractDocument)old).readUnlock(); } } revalidate(); repaint(); if (accessibleContext != null) { model.addDocumentListener( ((AccessibleJTextComponent)accessibleContext)); } if (inputMethodRequestsHandler != null) { model.addDocumentListener((DocumentListener)inputMethodRequestsHandler); } } /** * Fetches the model associated with the editor. This is * primarily for the UI to get at the minimal amount of * state required to be a text editor. Subclasses will * return the actual type of the model which will typically * be something that extends Document. * * @return the model */ public Document getDocument() { return model; } // Override of Component.setComponentOrientation public void setComponentOrientation( ComponentOrientation o ) { // Set the document's run direction property to match the // ComponentOrientation property. Document doc = getDocument(); if( doc != null ) { Boolean runDir = o.isLeftToRight() ? TextAttribute.RUN_DIRECTION_LTR : TextAttribute.RUN_DIRECTION_RTL; doc.putProperty( TextAttribute.RUN_DIRECTION, runDir ); } super.setComponentOrientation( o ); } /** * Fetches the command list for the editor. This is * the list of commands supported by the plugged-in UI * augmented by the collection of commands that the * editor itself supports. These are useful for binding * to events, such as in a keymap. * * @return the command list */ public Action[] getActions() { return getUI().getEditorKit(this).getActions(); } /** * Sets margin space between the text component's border * and its text. The text component's default Border * 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 Border object's responsibility to create the * appropriate margin space (else this property will effectively * be ignored). This causes a redraw of the component. * A PropertyChange event ("margin") is sent to all listeners. * * @param m the space between the border and the text * @beaninfo * description: desired space between the border and text area * bound: true */ public void setMargin(Insets m) { Insets old = margin; margin = m; firePropertyChange("margin", old, m); invalidate(); } /** * Returns the margin between the text component's border and * its text. * * @return the margin */ public Insets getMargin() { return margin; } /** * Sets the NavigationFilter. NavigationFilter * is used by DefaultCaret and the default cursor movement * actions as a way to restrict the cursor movement. * * @since 1.4 */ public void setNavigationFilter(NavigationFilter filter) { navigationFilter = filter; } /** * Returns the NavigationFilter. NavigationFilter * is used by DefaultCaret and the default cursor movement * actions as a way to restrict the cursor movement. A null return value * implies the cursor movement and selection should not be restricted. * * @since 1.4 * @return the NavigationFilter */ public NavigationFilter getNavigationFilter() { return navigationFilter; } /** * Fetches the caret that allows text-oriented navigation over * the view. * * @return the caret */ @Transient public Caret getCaret() { return caret; } /** * Sets the caret to be used. By default this will be set * by the UI that gets installed. This can be changed to * a custom caret if desired. Setting the caret results in a * PropertyChange event ("caret") being fired. * * @param c the caret * @see #getCaret * @beaninfo * description: the caret used to select/navigate * bound: true * expert: true */ public void setCaret(Caret c) { if (caret != null) { caret.removeChangeListener(caretEvent); caret.deinstall(this); } Caret old = caret; caret = c; if (caret != null) { caret.install(this); caret.addChangeListener(caretEvent); } firePropertyChange("caret", old, caret); } /** * Fetches the object responsible for making highlights. * * @return the highlighter */ public Highlighter getHighlighter() { return highlighter; } /** * Sets the highlighter to be used. By default this will be set * by the UI that gets installed. This can be changed to * a custom highlighter if desired. The highlighter can be set to * null to disable it. * A PropertyChange event ("highlighter") is fired * when a new highlighter is installed. * * @param h the highlighter * @see #getHighlighter * @beaninfo * description: object responsible for background highlights * bound: true * expert: true */ public void setHighlighter(Highlighter h) { if (highlighter != null) { highlighter.deinstall(this); } Highlighter old = highlighter; highlighter = h; if (highlighter != null) { highlighter.install(this); } firePropertyChange("highlighter", old, h); } /** * Sets the keymap to use for binding events to * actions. Setting to null effectively disables * keyboard input. * A PropertyChange event ("keymap") is fired when a new keymap * is installed. * * @param map the keymap * @see #getKeymap * @beaninfo * description: set of key event to action bindings to use * bound: true */ public void setKeymap(Keymap map) { Keymap old = keymap; keymap = map; firePropertyChange("keymap", old, keymap); updateInputMap(old, map); } /** * Turns on or off automatic drag handling. In order to enable automatic * drag handling, this property should be set to {@code true}, and the * component's {@code TransferHandler} needs to be {@code non-null}. * The default value of the {@code dragEnabled} property is {@code false}. *

* 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. *

* 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 * b is true and * GraphicsEnvironment.isHeadless() * returns true * @see java.awt.GraphicsEnvironment#isHeadless * @see #getDragEnabled * @see #setTransferHandler * @see TransferHandler * @since 1.4 * * @beaninfo * description: determines whether automatic drag handling is enabled * bound: false */ public void setDragEnabled(boolean b) { if (b && GraphicsEnvironment.isHeadless()) { throw new HeadlessException(); } dragEnabled = b; } /** * Returns whether or not automatic drag handling is enabled. * * @return the value of the {@code dragEnabled} property * @see #setDragEnabled * @since 1.4 */ public boolean getDragEnabled() { return dragEnabled; } /** * Sets the drop mode for this component. For backward compatibility, * the default for this property is DropMode.USE_SELECTION. * Usage of DropMode.INSERT 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. *

* JTextComponents support the following drop modes: *

*

* The drop mode is only meaningful if this component has a * TransferHandler that accepts drops. * * @param dropMode the drop mode to use * @throws IllegalArgumentException if the drop mode is unsupported * or null * @see #getDropMode * @see #getDropLocation * @see #setTransferHandler * @see javax.swing.TransferHandler * @since 1.6 */ public final void setDropMode(DropMode dropMode) { if (dropMode != null) { switch (dropMode) { case USE_SELECTION: case INSERT: this.dropMode = dropMode; return; } } throw new IllegalArgumentException(dropMode + ": Unsupported drop mode for text"); } /** * Returns the drop mode for this component. * * @return the drop mode for this component * @see #setDropMode * @since 1.6 */ public final DropMode getDropMode() { return dropMode; } static { SwingAccessor.setJTextComponentAccessor( new SwingAccessor.JTextComponentAccessor() { public TransferHandler.DropLocation dropLocationForPoint(JTextComponent textComp, Point p) { return textComp.dropLocationForPoint(p); } public Object setDropLocation(JTextComponent textComp, TransferHandler.DropLocation location, Object state, boolean forDrop) { return textComp.setDropLocation(location, state, forDrop); } }); } /** * Calculates a drop location in this component, representing where a * drop at the given point should insert data. *

* Note: This method is meant to override * JComponent.dropLocationForPoint(), which is package-private * in javax.swing. TransferHandler 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 null */ 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: *

* 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 null. 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 null since there's * no longer anything to store. *

* Note: This method is meant to override * JComponent.setDropLocation(), which is package-private * in javax.swing. TransferHandler 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 * dropLocationForPoint) or null * if there's no longer a valid drop location * @param state the state object saved earlier for this component, * or null * @param forDrop whether or not the method is being called because an * actual drop occurred * @return any saved state for this component, or null 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. *

* 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 canImport * has returned and has allowed for the location to be shown. *

* When this property changes, a property change event with * name "dropLocation" is fired by the component. * * @return the drop location * @see #setDropMode * @see TransferHandler#canImport(TransferHandler.TransferSupport) * @since 1.6 */ public final DropLocation getDropLocation() { return dropLocation; } /** * Updates the InputMaps in response to a * Keymap change. * @param oldKm the old Keymap * @param newKm the new Keymap */ 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 null 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 null 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 null 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 null-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 getKeymapTable() { synchronized (KEYMAP_TABLE) { AppContext appContext = AppContext.getAppContext(); HashMap keymapTable = (HashMap)appContext.get(KEYMAP_TABLE); if (keymapTable == null) { keymapTable = new HashMap(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. *

* Warning: * 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 JavaBeansTM * has been added to the java.beans package. * Please see {@link java.beans.XMLEncoder}. */ public static class KeyBinding { /** * The key. */ public KeyStroke key; /** * The name of the action for the key. */ public String actionName; /** * Creates a new key binding. * * @param key the key * @param actionName the name of the action for the key */ public KeyBinding(KeyStroke key, String actionName) { this.key = key; this.actionName = actionName; } } /** *

* 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: *


     *
     *   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());
     *
     * 
* The sets of bindings and actions may be empty but must be * non-null. * * @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 h = new Hashtable(); 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); } } } /** * Returns true if klass is NOT a JTextComponent and it or * one of its superclasses (stoping at JTextComponent) overrides * processInputMethodEvent. It is assumed this will be * invoked from within a doPrivileged, and it is also * assumed klass extends JTextComponent. */ private static Boolean isProcessInputMethodEventOverridden(Class klass) { if (klass == JTextComponent.class) { return Boolean.FALSE; } Boolean retValue = overrideMap.get(klass.getName()); if (retValue != null) { return retValue; } Boolean sOverriden = isProcessInputMethodEventOverridden( klass.getSuperclass()); if (sOverriden.booleanValue()) { // If our superclass has overriden it, then by definition klass // overrides it. overrideMap.put(klass.getName(), sOverriden); return sOverriden; } // klass's superclass didn't override it, check for an override in // klass. try { Class[] classes = new Class[1]; classes[0] = InputMethodEvent.class; Method m = klass.getDeclaredMethod("processInputMethodEvent", classes); retValue = Boolean.TRUE; } catch (NoSuchMethodException nsme) { retValue = Boolean.FALSE; } overrideMap.put(klass.getName(), retValue); return retValue; } /** * 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 null effectively restores the default color. * Setting the color results in a PropertyChange event ("caretColor") * being fired. * * @param c the color * @see #getCaretColor * @beaninfo * description: the color used to render the caret * bound: true * preferred: true */ public void setCaretColor(Color c) { Color old = caretColor; caretColor = c; firePropertyChange("caretColor", old, caretColor); } /** * Fetches the current color used to render the * selection. * * @return the color */ public Color getSelectionColor() { return selectionColor; } /** * Sets the current color used to render the selection. * Setting the color to null is the same as setting * Color.white. Setting the color results in a * PropertyChange event ("selectionColor"). * * @param c the color * @see #getSelectionColor * @beaninfo * description: color used to render selection background * bound: true * preferred: true */ public void setSelectionColor(Color c) { Color old = selectionColor; selectionColor = c; firePropertyChange("selectionColor", old, selectionColor); } /** * Fetches the current color used to render the * selected text. * * @return the color */ public Color getSelectedTextColor() { return selectedTextColor; } /** * Sets the current color used to render the selected text. * Setting the color to null is the same as * Color.black. Setting the color results in a * PropertyChange event ("selectedTextColor") being fired. * * @param c the color * @see #getSelectedTextColor * @beaninfo * description: color used to render selected text * bound: true * preferred: true */ public void setSelectedTextColor(Color c) { Color old = selectedTextColor; selectedTextColor = c; firePropertyChange("selectedTextColor", old, selectedTextColor); } /** * Fetches the current color used to render the * disabled text. * * @return the color */ public Color getDisabledTextColor() { return disabledTextColor; } /** * Sets the current color used to render the * disabled text. Setting the color fires off a * PropertyChange event ("disabledTextColor"). * * @param c the color * @see #getDisabledTextColor * @beaninfo * description: color used to render disabled text * bound: true * preferred: true */ public void setDisabledTextColor(Color c) { Color old = disabledTextColor; disabledTextColor = c; firePropertyChange("disabledTextColor", old, disabledTextColor); } /** * Replaces the currently selected content with new content * represented by the given string. If there is no selection * this amounts to an insert of the given text. If there * is no replacement text this amounts to a removal of the * current selection. *

* This is the method that is used by the default implementation * of the action for inserting content that gets bound to the * keymap actions. * * @param content the content to replace the selection with */ public void replaceSelection(String content) { Document doc = getDocument(); if (doc != null) { try { boolean composedTextSaved = saveComposedText(caret.getDot()); int p0 = Math.min(caret.getDot(), caret.getMark()); int p1 = Math.max(caret.getDot(), caret.getMark()); if (doc instanceof AbstractDocument) { ((AbstractDocument)doc).replace(p0, p1 - p0, content,null); } else { if (p0 != p1) { doc.remove(p0, p1 - p0); } if (content != null && content.length() > 0) { doc.insertString(p0, content, null); } } if (composedTextSaved) { restoreComposedText(); } } catch (BadLocationException e) { UIManager.getLookAndFeel().provideErrorFeedback(JTextComponent.this); } } } /** * Fetches a portion of the text represented by the * component. Returns an empty string if length is 0. * * @param offs the offset >= 0 * @param len the length >= 0 * @return the text * @exception BadLocationException if the offset or length are invalid */ public String getText(int offs, int len) throws BadLocationException { return getDocument().getText(offs, len); } /** * Converts the given location in the model to a place in * the view coordinate system. * The component must have a positive size for * this translation to be computed (i.e. layout cannot * be computed until the component has been sized). The * component does not have to be visible or painted. * * @param pos the position >= 0 * @return the coordinates as a rectangle, with (r.x, r.y) as the location * in the coordinate system, or null if the component does * not yet have a positive size. * @exception BadLocationException if the given position does not * represent a valid location in the associated document * @see TextUI#modelToView */ public Rectangle modelToView(int pos) throws BadLocationException { return getUI().modelToView(this, pos); } /** * Converts the given place in the view coordinate system * to the nearest representative location in the model. * The component must have a positive size for * this translation to be computed (i.e. layout cannot * be computed until the component has been sized). The * component does not have to be visible or painted. * * @param pt the location in the view to translate * @return the offset >= 0 from the start of the document, * or -1 if the component does not yet have a positive * size. * @see TextUI#viewToModel */ public int viewToModel(Point pt) { return getUI().viewToModel(this, pt); } /** * Transfers the currently selected range in the associated * text model to the system clipboard, removing the contents * from the model. The current selection is reset. Does nothing * for null 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 null 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 conveniance method that is only useful for * cut, copy and paste. If * an Action with the name name does not * exist in the ActionMap, this will attemp to install a * TransferHandler and then use altAction. */ 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 TransferHandler 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 setCaretPosition was * called. This forms a selection. * If the document is null, 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 position 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 alt key and the character * given (converted to upper case). By default, there is no focus * accelerator key. Any previous key accelerator setting will be * superseded. A '\0' key setting will be registered, and has the * effect of turning off the focus accelerator. When the new key * is set, a PropertyChange event (FOCUS_ACCELERATOR_KEY) will be fired. * * @param aKey the key * @see #getFocusAccelerator * @beaninfo * description: accelerator character used to grab focus * bound: true */ public void setFocusAccelerator(char aKey) { aKey = Character.toUpperCase(aKey); char old = focusAccelerator; focusAccelerator = aKey; // Fix for 4341002: value of FOCUS_ACCELERATOR_KEY is wrong. // So we fire both FOCUS_ACCELERATOR_KEY, for compatibility, // and the correct event here. firePropertyChange(FOCUS_ACCELERATOR_KEY, old, focusAccelerator); firePropertyChange("focusAccelerator", old, focusAccelerator); } /** * Returns the key accelerator that will cause the receiving * text component to get the focus. Return '\0' if no focus * accelerator has been set. * * @return the key */ public char getFocusAccelerator() { return focusAccelerator; } /** * Initializes from a stream. This creates a * model of the type appropriate for the component * and initializes the model from the stream. * By default this will load the model as plain * text. Previous contents of the model are discarded. * * @param in the stream to read from * @param desc an object describing the stream; this * might be a string, a File, a URL, etc. Some kinds * of documents (such as html for example) might be * able to make use of this information; if non-null, * 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 * TextComponent. Note that the caret tracks change, * so this may move if the underlying text of the component is changed. * If the document is null, 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 position is less than zero or greater * than the component's text length * @beaninfo * description: the caret position */ public void setCaretPosition(int position) { Document doc = getDocument(); if (doc != null) { if (position > doc.getLength() || position < 0) { throw new IllegalArgumentException("bad position: " + position); } caret.setDot(position); } } /** * Returns the position of the text insertion caret for the * text component. * * @return the position of the text insertion caret for the * text component >= 0 */ @Transient public int getCaretPosition() { return caret.getDot(); } /** * Sets the text of this TextComponent * to the specified text. If the text is null * 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. * *

* Note that text is not a bound property, so no PropertyChangeEvent * is fired when it changes. To listen for changes to the text, * use DocumentListener. * * @param t the new text to be set * @see #getText * @see DefaultCaret * @beaninfo * description: the text of this component */ public void setText(String t) { try { Document doc = getDocument(); if (doc instanceof AbstractDocument) { ((AbstractDocument)doc).replace(0, doc.getLength(), t,null); } else { doc.remove(0, doc.getLength()); doc.insertString(0, t, null); } } catch (BadLocationException e) { UIManager.getLookAndFeel().provideErrorFeedback(JTextComponent.this); } } /** * Returns the text contained in this TextComponent. * If the underlying document is null, * will give a NullPointerException. * * Note that text is not a bound property, so no PropertyChangeEvent * is fired when it changes. To listen for changes to the text, * use DocumentListener. * * @return the text * @exception NullPointerException if the document is null * @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 * TextComponent. If the selection is * null or the document empty, returns null. * * @return the text * @exception IllegalArgumentException if the selection doesn't * have a valid mapping into the document for some reason * @see #setText */ public String getSelectedText() { String txt = null; int p0 = Math.min(caret.getDot(), caret.getMark()); int p1 = Math.max(caret.getDot(), caret.getMark()); if (p0 != p1) { try { Document doc = getDocument(); txt = doc.getText(p0, p1 - p0); } catch (BadLocationException e) { throw new IllegalArgumentException(e.getMessage()); } } return txt; } /** * Returns the boolean indicating whether this * TextComponent 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 * TextComponent should be editable. * A PropertyChange event ("editable") is fired when the * state is changed. * * @param b the boolean to be set * @see #isEditable * @beaninfo * description: specifies if the text can be edited * bound: true */ public void setEditable(boolean b) { if (b != editable) { boolean oldVal = editable; editable = b; enableInputMethods(editable); firePropertyChange("editable", Boolean.valueOf(oldVal), Boolean.valueOf(editable)); repaint(); } } /** * Returns the selected text's start position. Return 0 for an * empty document, or the value of dot if no selection. * * @return the start position >= 0 */ @Transient public int getSelectionStart() { int start = Math.min(caret.getDot(), caret.getMark()); return start; } /** * Sets the selection start to the specified position. The new * starting point is constrained to be before or at the current * selection end. *

* This is available for backward compatibility to code * that called this method on java.awt.TextComponent. * This is implemented to forward to the Caret * implementation which is where the actual selection is maintained. * * @param selectionStart the start position of the text >= 0 * @beaninfo * description: starting location of the selection. */ public void setSelectionStart(int selectionStart) { /* Route through select method to enforce consistent policy * between selectionStart and selectionEnd. */ select(selectionStart, getSelectionEnd()); } /** * Returns the selected text's end position. Return 0 if the document * is empty, or the value of dot if there is no selection. * * @return the end position >= 0 */ @Transient public int getSelectionEnd() { int end = Math.max(caret.getDot(), caret.getMark()); return end; } /** * Sets the selection end to the specified position. The new * end point is constrained to be at or after the current * selection start. *

* This is available for backward compatibility to code * that called this method on java.awt.TextComponent. * This is implemented to forward to the Caret * implementation which is where the actual selection is maintained. * * @param selectionEnd the end position of the text >= 0 * @beaninfo * description: ending location of the selection. */ public void setSelectionEnd(int selectionEnd) { /* Route through select method to enforce consistent policy * between selectionStart and selectionEnd. */ select(getSelectionStart(), selectionEnd); } /** * Selects the text between the specified start and end positions. *

* 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. *

* 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. *

* This call is provided for backward compatibility. * It is routed to a call to setCaretPosition * followed by a call to moveCaretPosition. * 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 TextComponent. * Does nothing on a null 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 event. * This will return one of: *

    *
  1. If setToolTipText has been invoked with a * non-null * value, it will be returned, otherwise *
  2. The value from invoking getToolTipText on * the UI will be returned. *
* By default JTextComponent does not register * itself with the ToolTipManager. * This means that tooltips will NOT be shown from the * TextUI unless registerComponent has * been invoked on the ToolTipManager. * * @param event the event in question * @return the string to be used as the tooltip for event * @see javax.swing.JComponent#setToolTipText * @see javax.swing.plaf.TextUI#getToolTipText * @see javax.swing.ToolTipManager#registerComponent */ public String getToolTipText(MouseEvent event) { String retValue = super.getToolTipText(event); if (retValue == null) { TextUI ui = getUI(); if (ui != null) { retValue = ui.getToolTipText(this, new Point(event.getX(), event.getY())); } } return retValue; } // --- Scrollable methods --------------------------------------------- /** * Returns the preferred size of the viewport for a view component. * This is implemented to do the default behavior of returning * the preferred size of the component. * * @return the preferredSize of a JViewport * whose view is this Scrollable */ 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. *

* 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 SwingConstants.VERTICAL or * SwingConstants.HORIZONTAL * @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. *

* 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 SwingConstants.VERTICAL or * SwingConstants.HORIZONTAL * @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 * Scrollable 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 * Scrollable whose ancestor is a JScrollPane * effectively disables horizontal scrolling. *

* Scrolling containers, like JViewport, * will use this method each time they are validated. * * @return true if a viewport should force the Scrollables * width to match its own */ public boolean getScrollableTracksViewportWidth() { Container parent = SwingUtilities.getUnwrappedParent(this); if (parent instanceof JViewport) { return parent.getWidth() > getPreferredSize().width; } return false; } /** * Returns true if a viewport should always force the height of this * Scrollable 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. *

* Scrolling containers, like JViewport, * will use this method each time they are validated. * * @return true if a viewport should force the Scrollables height * to match its own */ public boolean getScrollableTracksViewportHeight() { Container parent = SwingUtilities.getUnwrappedParent(this); if (parent instanceof JViewport) { return parent.getHeight() > getPreferredSize().height; } return false; } ////////////////// // Printing Support ////////////////// /** * A convenience print method that displays a print dialog, and then * prints this {@code JTextComponent} in interactive mode with no * header or footer text. Note: this method * blocks until printing is done. *

* Note: In headless mode, no dialogs will be shown. * *

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 interactive mode with * the specified header and footer text. Note: this method * blocks until printing is done. *

* Note: In headless mode, no dialogs will be shown. * *

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. * *

* 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. * *

* {@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. * *

* {@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. * *

* {@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 * duplex or single-sided. * *

* {@code interactive boolean} parameter allows you to specify * whether to perform printing in interactive * mode. If {@code true}, a progress dialog, with an abort option, * is displayed for the duration of printing. This dialog is * modal when {@code print} is invoked on the Event Dispatch * Thread and non-modal otherwise. Warning: * calling this method on the Event Dispatch Thread with {@code * interactive false} blocks all events, including repaints, from * being processed until printing is complete. It is only * recommended when printing from an application with no * visible GUI. * *

* Note: In headless mode, {@code showPrintDialog} and * {@code interactive} parameters are ignored and no dialogs are * shown. * *

* 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. * *

* This method uses {@link #getPrintable} to render document content. * *

* This method is thread-safe, although most Swing methods are not. Please * see * How to Use Threads for more information. * *

* Sample Usage. This code snippet shows a cross-platform print * dialog and then prints the {@code JTextComponent} in interactive mode * unless the user cancels the dialog: * *

     * textComponent.print(new MessageFormat("My text component header"),
     *     new MessageFormat("Footer. Page - {0}"), true, null, null, true);
     * 
*

* Executing this code off the Event Dispatch Thread * performs printing on the background. * The following pattern might be used for background * printing: *

     *     FutureTask<Boolean> future =
     *         new FutureTask<Boolean>(
     *             new Callable<Boolean>() {
     *                 public Boolean call() {
     *                     return textComponent.print(.....);
     *                 }
     *             });
     *     executor.execute(future);
     * 
* * @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 doPrint = new Callable() { public Object call() throws Exception { try { job.print(attr); } finally { if (printingStatus != null) { printingStatus.dispose(); } } return null; } }; final FutureTask futurePrinting = new FutureTask(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() { public Boolean call() throws Exception { boolean rv = isEnabled(); if (rv) { setEnabled(false); } return rv; } }).get(); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (ExecutionException e) { Throwable cause = e.getCause(); if (cause instanceof Error) { throw (Error) cause; } if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } throw new AssertionError(cause); } } getDocument().render(futurePrinting); //enable component if (wasEnabled) { if (isEventDispatchThread) { setEnabled(true); } else { try { SwingUtilities2.submit( new Runnable() { public void run() { setEnabled(true); } }, null).get(); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (ExecutionException e) { Throwable cause = e.getCause(); if (cause instanceof Error) { throw (Error) cause; } if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } throw new AssertionError(cause); } } } } }; if (! interactive || isHeadless) { runnablePrinting.run(); } else { if (isEventDispatchThread) { (new Thread(runnablePrinting)).start(); printingStatus.showModal(true); } else { printingStatus.showModal(false); runnablePrinting.run(); } } //the printing is done successfully or otherwise. //dialog is hidden if needed. try { futurePrinting.get(); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (ExecutionException e) { Throwable cause = e.getCause(); if (cause instanceof PrinterAbortException) { if (printingStatus != null && printingStatus.isAborted()) { return false; } else { throw (PrinterAbortException) cause; } } else if (cause instanceof PrinterException) { throw (PrinterException) cause; } else if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } else if (cause instanceof Error) { throw (Error) cause; } else { throw new AssertionError(cause); } } return true; } /** * Returns a {@code Printable} to use for printing the content of this * {@code JTextComponent}. The returned {@code Printable} prints * the document as it looks on the screen except being reformatted * to fit the paper. * The returned {@code Printable} can be wrapped inside another * {@code Printable} in order to create complex reports and * documents. * * *

* 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. * *

* 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. * *

* 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}. * *

* This method is thread-safe, although most Swing methods are not. Please * see * How to Use Threads for more information. * *

* The returned {@code Printable} can be printed on any thread. * *

* This implementation returned {@code Printable} performs all painting on * the Event Dispatch Thread, 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 AccessibleContext associated with this * JTextComponent. For text components, * the AccessibleContext takes the form of an * AccessibleJTextComponent. * A new AccessibleJTextComponent instance * is created if necessary. * * @return an AccessibleJTextComponent that serves as the * AccessibleContext of this * JTextComponent */ public AccessibleContext getAccessibleContext() { if (accessibleContext == null) { accessibleContext = new AccessibleJTextComponent(); } return accessibleContext; } /** * This class implements accessibility support for the * JTextComponent class. It provides an implementation of * the Java Accessibility API appropriate to menu user-interface elements. *

* Warning: * 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 JavaBeansTM * has been added to the java.beans package. * Please see {@link java.beans.XMLEncoder}. */ public class AccessibleJTextComponent extends AccessibleJComponent implements AccessibleText, CaretListener, DocumentListener, AccessibleAction, AccessibleEditableText, AccessibleExtendedText { int caretPos; Point oldLocationOnScreen; /** * Constructs an AccessibleJTextComponent. Adds a listener to track * caret change. */ public AccessibleJTextComponent() { Document doc = JTextComponent.this.getDocument(); if (doc != null) { doc.addDocumentListener(this); } JTextComponent.this.addCaretListener(this); caretPos = getCaretPosition(); try { oldLocationOnScreen = getLocationOnScreen(); } catch (IllegalComponentStateException iae) { } // Fire a ACCESSIBLE_VISIBLE_DATA_PROPERTY PropertyChangeEvent // when the text component moves (e.g., when scrolling). // Using an anonymous class since making AccessibleJTextComponent // implement ComponentListener would be an API change. JTextComponent.this.addComponentListener(new ComponentAdapter() { public void componentMoved(ComponentEvent e) { try { Point newLocationOnScreen = getLocationOnScreen(); firePropertyChange(ACCESSIBLE_VISIBLE_DATA_PROPERTY, oldLocationOnScreen, newLocationOnScreen); oldLocationOnScreen = newLocationOnScreen; } catch (IllegalComponentStateException iae) { } } }); } /** * Handles caret updates (fire appropriate property change event, * which are AccessibleContext.ACCESSIBLE_CARET_PROPERTY and * AccessibleContext.ACCESSIBLE_SELECTION_PROPERTY). * This keeps track of the dot position internally. When the caret * moves, the internal position is updated after firing the event. * * @param e the CaretEvent */ public void caretUpdate(CaretEvent e) { int dot = e.getDot(); int mark = e.getMark(); if (caretPos != dot) { // the caret moved firePropertyChange(ACCESSIBLE_CARET_PROPERTY, new Integer(caretPos), new Integer(dot)); caretPos = dot; try { oldLocationOnScreen = getLocationOnScreen(); } catch (IllegalComponentStateException iae) { } } if (mark != dot) { // there is a selection firePropertyChange(ACCESSIBLE_SELECTION_PROPERTY, null, getSelectedText()); } } // DocumentListener methods /** * Handles document insert (fire appropriate property change event * which is AccessibleContext.ACCESSIBLE_TEXT_PROPERTY). * This tracks the changed offset via the event. * * @param e the DocumentEvent */ public void insertUpdate(DocumentEvent e) { final Integer pos = new Integer (e.getOffset()); if (SwingUtilities.isEventDispatchThread()) { firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, pos); } else { Runnable doFire = new Runnable() { public void run() { firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, pos); } }; SwingUtilities.invokeLater(doFire); } } /** * Handles document remove (fire appropriate property change event, * which is AccessibleContext.ACCESSIBLE_TEXT_PROPERTY). * This tracks the changed offset via the event. * * @param e the DocumentEvent */ public void removeUpdate(DocumentEvent e) { final Integer pos = new Integer (e.getOffset()); if (SwingUtilities.isEventDispatchThread()) { firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, pos); } else { Runnable doFire = new Runnable() { public void run() { firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, pos); } }; SwingUtilities.invokeLater(doFire); } } /** * Handles document remove (fire appropriate property change event, * which is AccessibleContext.ACCESSIBLE_TEXT_PROPERTY). * This tracks the changed offset via the event. * * @param e the DocumentEvent */ public void changedUpdate(DocumentEvent e) { final Integer pos = new Integer (e.getOffset()); if (SwingUtilities.isEventDispatchThread()) { firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, pos); } else { Runnable doFire = new Runnable() { public void run() { firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, pos); } }; SwingUtilities.invokeLater(doFire); } } /** * Gets the state set of the JTextComponent. * The AccessibleStateSet of an object is composed of a set of * unique AccessibleState's. A change in the AccessibleStateSet * of an object will cause a PropertyChangeEvent to be fired * for the AccessibleContext.ACCESSIBLE_STATE_PROPERTY property. * * @return an instance of AccessibleStateSet containing the * current state set of the object * @see AccessibleStateSet * @see AccessibleState * @see #addPropertyChangeListener */ public AccessibleStateSet getAccessibleStateSet() { AccessibleStateSet states = super.getAccessibleStateSet(); if (JTextComponent.this.isEditable()) { states.add(AccessibleState.EDITABLE); } return states; } /** * Gets the role of this object. * * @return an instance of AccessibleRole describing the role of the * object (AccessibleRole.TEXT) * @see AccessibleRole */ public AccessibleRole getAccessibleRole() { return AccessibleRole.TEXT; } /** * Get the AccessibleText associated with this object. In the * implementation of the Java Accessibility API for this class, * return this object, which is responsible for implementing the * AccessibleText interface on behalf of itself. * * @return this object */ public AccessibleText getAccessibleText() { return this; } // --- interface AccessibleText methods ------------------------ /** * Many of these methods are just convenience methods; they * just call the equivalent on the parent */ /** * Given a point in local coordinates, return the zero-based index * of the character under that Point. If the point is invalid, * this method returns -1. * * @param p the Point in local coordinates * @return the zero-based index of the character under Point p. */ public int getIndexAtPoint(Point p) { if (p == null) { return -1; } return JTextComponent.this.viewToModel(p); } /** * Gets the editor's drawing rectangle. Stolen * from the unfortunately named * BasicTextUI.getVisibleEditorRect() * * @return the bounding box for the root view */ Rectangle getRootEditorRect() { Rectangle alloc = JTextComponent.this.getBounds(); if ((alloc.width > 0) && (alloc.height > 0)) { alloc.x = alloc.y = 0; Insets insets = JTextComponent.this.getInsets(); alloc.x += insets.left; alloc.y += insets.top; alloc.width -= insets.left + insets.right; alloc.height -= insets.top + insets.bottom; return alloc; } return null; } /** * Determines the bounding box of the character at the given * index into the string. The bounds are returned in local * coordinates. If the index is invalid a null rectangle * is returned. * * The screen coordinates returned are "unscrolled coordinates" * if the JTextComponent is contained in a JScrollPane in which * case the resulting rectangle should be composed with the parent * coordinates. A good algorithm to use is: * * 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; * * * 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 teh 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 Segment 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 index. * If direction 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 Segment containing the paragraph text * at index, or null if index 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 index representing either * the paragraph or sentence as identified by part, 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 index. * If direction is non-null this will find the * next/previous word/sentence/character. * * @param part the CHARACTER, WORD, * SENTENCE, LINE or * ATTRIBUTE_RUN to retrieve * @param index an index within the text * @param direction is either -1, 0, or 1 * @return an AccessibleTextSequence specifying the text * if part and index are valid. Otherwise, * null 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 index, and going in * direction, return the edge of run that shares the * same AttributeSet and parent element as those at * index. * * 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 AccessibleTextSequence at a given * index. * * @param part the CHARACTER, WORD, * SENTENCE, LINE or * ATTRIBUTE_RUN to retrieve * @param index an index within the text * @return an AccessibleTextSequence specifying the text if * part and index are valid. Otherwise, * null 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 AccessibleTextSequence after a given * index. * * @param part the CHARACTER, WORD, * SENTENCE, LINE or * ATTRIBUTE_RUN to retrieve * @param index an index within the text * @return an AccessibleTextSequence specifying the text * if part and index are valid. Otherwise, * null 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 AccessibleTextSequence before a given * index. * * @param part the CHARACTER, WORD, * SENTENCE, LINE or * ATTRIBUTE_RUN to retrieve * @param index an index within the text * @return an AccessibleTextSequence specifying the text * if part and index are valid. Otherwise, * null 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 Rectangle 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, null is returned * * @since 1.6 */ public Rectangle getTextBounds(int startIndex, int endIndex) { if (startIndex < 0 || startIndex > model.getLength()-1 || endIndex < 0 || endIndex > model.getLength()-1 || startIndex > endIndex) { return null; } TextUI ui = getUI(); if (ui == null) { return null; } Rectangle rect = null; Rectangle alloc = getRootEditorRect(); if (alloc == null) { return null; } if (model instanceof AbstractDocument) { ((AbstractDocument)model).readLock(); } try { View rootView = ui.getRootView(JTextComponent.this); if (rootView != null) { Shape bounds = rootView.modelToView(startIndex, Position.Bias.Forward, endIndex, Position.Bias.Backward, alloc); rect = (bounds instanceof Rectangle) ? (Rectangle)bounds : bounds.getBounds(); } } catch (BadLocationException e) { } finally { if (model instanceof AbstractDocument) { ((AbstractDocument)model).readUnlock(); } } return rect; } // ----- end AccessibleExtendedText methods // --- interface AccessibleAction methods ------------------------ public AccessibleAction getAccessibleAction() { return this; } /** * Returns the number of accessible actions available in this object * If there are more than one, the first one is considered the * "default" action of the object. * * @return the zero-based number of Actions in this object * @since 1.4 */ public int getAccessibleActionCount() { Action [] actions = JTextComponent.this.getActions(); return actions.length; } /** * Returns a description of the specified action of the object. * * @param i zero-based index of the actions * @return a String description of the action * @see #getAccessibleActionCount * @since 1.4 */ public String getAccessibleActionDescription(int i) { Action [] actions = JTextComponent.this.getActions(); if (i < 0 || i >= actions.length) { return null; } return (String)actions[i].getValue(Action.NAME); } /** * Performs the specified Action on the object * * @param i zero-based index of actions * @return true if the action was performed; otherwise false. * @see #getAccessibleActionCount * @since 1.4 */ public boolean doAccessibleAction(int i) { Action [] actions = JTextComponent.this.getActions(); if (i < 0 || i >= actions.length) { return false; } ActionEvent ae = new ActionEvent(JTextComponent.this, ActionEvent.ACTION_PERFORMED, null, EventQueue.getMostRecentEventTime(), getCurrentEventModifiers()); actions[i].actionPerformed(ae); return true; } // ----- end AccessibleAction methods } // --- serialization --------------------------------------------- private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); caretEvent = new MutableCaretEvent(this); addMouseListener(caretEvent); addFocusListener(caretEvent); } // --- member variables ---------------------------------- /** * The document model. */ private Document model; /** * The caret used to display the insert position * and navigate throughout the document. * * PENDING(prinz) * This should be serializable, default installed * by UI. */ private transient Caret caret; /** * Object responsible for restricting the cursor navigation. */ private NavigationFilter navigationFilter; /** * The object responsible for managing highlights. * * PENDING(prinz) * This should be serializable, default installed * by UI. */ private transient Highlighter highlighter; /** * The current key bindings in effect. * * PENDING(prinz) * This should be serializable, default installed * by UI. */ private transient Keymap keymap; private transient MutableCaretEvent caretEvent; private Color caretColor; private Color selectionColor; private Color selectedTextColor; private Color disabledTextColor; private boolean editable; private Insets margin; private char focusAccelerator; private boolean dragEnabled; /** * The drop mode for this component. */ private DropMode dropMode = DropMode.USE_SELECTION; /** * The drop location. */ private transient DropLocation dropLocation; /** * Represents a drop location for JTextComponents. * * @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 * processInputMethodEvent has been overriden. */ private static Map overrideMap; /** * Returns a string representation of this JTextComponent. * 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 null. *

* Overriding paramString to provide information about the * specific new aspects of the JFC components. * * @return a string representation of this JTextComponent */ protected String paramString() { String editableString = (editable ? "true" : "false"); String caretColorString = (caretColor != null ? caretColor.toString() : ""); String selectionColorString = (selectionColor != null ? selectionColor.toString() : ""); String selectedTextColorString = (selectedTextColor != null ? selectedTextColor.toString() : ""); String disabledTextColorString = (disabledTextColor != null ? disabledTextColor.toString() : ""); String marginString = (margin != null ? margin.toString() : ""); return super.paramString() + ",caretColor=" + caretColorString + ",disabledTextColor=" + disabledTextColorString + ",editable=" + editableString + ",margin=" + marginString + ",selectedTextColor=" + selectedTextColorString + ",selectionColor=" + selectionColorString; } /** * A Simple TransferHandler that exports the data as a String, and * imports the data from the String clipboard. This is only used * if the UI hasn't supplied one, which would only happen if someone * hasn't subclassed Basic. */ static class DefaultTransferHandler extends TransferHandler implements UIResource { public void exportToClipboard(JComponent comp, Clipboard clipboard, int action) throws IllegalStateException { if (comp instanceof JTextComponent) { JTextComponent text = (JTextComponent)comp; int p0 = text.getSelectionStart(); int p1 = text.getSelectionEnd(); if (p0 != p1) { try { Document doc = text.getDocument(); String srcData = doc.getText(p0, p1 - p0); StringSelection contents =new StringSelection(srcData); // this may throw an IllegalStateException, // but it will be caught and handled in the // action that invoked this method clipboard.setContents(contents, null); if (action == TransferHandler.MOVE) { doc.remove(p0, p1 - p0); } } catch (BadLocationException ble) {} } } } public boolean importData(JComponent comp, Transferable t) { if (comp instanceof JTextComponent) { DataFlavor flavor = getFlavor(t.getTransferDataFlavors()); if (flavor != null) { InputContext ic = comp.getInputContext(); if (ic != null) { ic.endComposition(); } try { String data = (String)t.getTransferData(flavor); ((JTextComponent)comp).replaceSelection(data); return true; } catch (UnsupportedFlavorException ufe) { } catch (IOException ioe) { } } } return false; } public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { JTextComponent c = (JTextComponent)comp; if (!(c.isEditable() && c.isEnabled())) { return false; } return (getFlavor(transferFlavors) != null); } public int getSourceActions(JComponent c) { return NONE; } private DataFlavor getFlavor(DataFlavor[] flavors) { if (flavors != null) { for (DataFlavor flavor : flavors) { if (flavor.equals(DataFlavor.stringFlavor)) { return flavor; } } } return null; } } /** * Returns the JTextComponent that most recently had focus. The returned * value may currently have focus. */ static final JTextComponent getFocusedComponent() { return (JTextComponent)AppContext.getAppContext(). get(FOCUSED_COMPONENT); } private int getCurrentEventModifiers() { int modifiers = 0; AWTEvent currentEvent = EventQueue.getCurrentEvent(); if (currentEvent instanceof InputEvent) { modifiers = ((InputEvent)currentEvent).getModifiers(); } else if (currentEvent instanceof ActionEvent) { modifiers = ((ActionEvent)currentEvent).getModifiers(); } return modifiers; } private static final Object KEYMAP_TABLE = new StringBuilder("JTextComponent_KeymapTable"); // // member variables used for on-the-spot input method // editing style support // private transient InputMethodRequests inputMethodRequestsHandler; private SimpleAttributeSet composedTextAttribute; private String composedTextContent; private Position composedTextStart; private Position composedTextEnd; private Position latestCommittedTextStart; private Position latestCommittedTextEnd; private ComposedTextCaret composedTextCaret; private transient Caret originalCaret; /** * Set to true after the check for the override of processInputMethodEvent * has been checked. */ private boolean checkedInputOverride; private boolean needToSendKeyTypedEvent; static class DefaultKeymap implements Keymap { DefaultKeymap(String nm, Keymap parent) { this.nm = nm; this.parent = parent; bindings = new Hashtable(); } /** * 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 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 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 keyStrokes = null; for (Enumeration keys = bindings.keys(); keys.hasMoreElements();) { KeyStroke key = keys.nextElement(); if (bindings.get(key) == a) { if (keyStrokes == null) { keyStrokes = new Vector(); } 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(); } 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 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 * keymap.getAction 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, * DefaultActionKey is returned. *

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 get is passed in * KeymapWrapper.DefaultActionKey, 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 * JTextComponent instances unless they * have had a different keymap set. */ public static final String DEFAULT_KEYMAP = "default"; /** * Event to use when firing a notification of change to caret * position. This is mutable so that the event can be reused * since caret events can be fairly high in bandwidth. */ static class MutableCaretEvent extends CaretEvent implements ChangeListener, FocusListener, MouseListener { MutableCaretEvent(JTextComponent c) { super(c); } final void fire() { JTextComponent c = (JTextComponent) getSource(); if (c != null) { Caret caret = c.getCaret(); dot = caret.getDot(); mark = caret.getMark(); c.fireCaretUpdate(this); } } public final String toString() { return "dot=" + dot + "," + "mark=" + mark; } // --- CaretEvent methods ----------------------- public final int getDot() { return dot; } public final int getMark() { return mark; } // --- ChangeListener methods ------------------- public final void stateChanged(ChangeEvent e) { if (! dragActive) { fire(); } } // --- FocusListener methods ----------------------------------- public void focusGained(FocusEvent fe) { AppContext.getAppContext().put(FOCUSED_COMPONENT, fe.getSource()); } public void focusLost(FocusEvent fe) { } // --- MouseListener methods ----------------------------------- /** * Requests focus on the associated * text component, and try to set the cursor position. * * @param e the mouse event * @see MouseListener#mousePressed */ public final void mousePressed(MouseEvent e) { dragActive = true; } /** * Called when the mouse is released. * * @param e the mouse event * @see MouseListener#mouseReleased */ public final void mouseReleased(MouseEvent e) { dragActive = false; fire(); } public final void mouseClicked(MouseEvent e) { } public final void mouseEntered(MouseEvent e) { } public final void mouseExited(MouseEvent e) { } private boolean dragActive; private int dot; private int mark; } // // Process any input method events that the component itself // recognizes. The default on-the-spot handling for input method // composed(uncommitted) text is done here after all input // method listeners get called for stealing the events. // protected void processInputMethodEvent(InputMethodEvent e) { // let listeners handle the events super.processInputMethodEvent(e); if (!e.isConsumed()) { if (! isEditable()) { return; } else { switch (e.getID()) { case InputMethodEvent.INPUT_METHOD_TEXT_CHANGED: replaceInputMethodText(e); // fall through case InputMethodEvent.CARET_POSITION_CHANGED: setInputMethodCaretPosition(e); break; } } e.consume(); } } // // Overrides this method to become an active input method client. // public InputMethodRequests getInputMethodRequests() { if (inputMethodRequestsHandler == null) { inputMethodRequestsHandler = new InputMethodRequestsHandler(); Document doc = getDocument(); if (doc != null) { doc.addDocumentListener((DocumentListener)inputMethodRequestsHandler); } } return inputMethodRequestsHandler; } // // Overrides this method to watch the listener installed. // public void addInputMethodListener(InputMethodListener l) { super.addInputMethodListener(l); if (l != null) { needToSendKeyTypedEvent = false; checkedInputOverride = true; } } // // Default implementation of the InputMethodRequests interface. // class InputMethodRequestsHandler implements InputMethodRequests, DocumentListener { // --- InputMethodRequests methods --- public AttributedCharacterIterator cancelLatestCommittedText( Attribute[] attributes) { Document doc = getDocument(); if ((doc != null) && (latestCommittedTextStart != null) && (!latestCommittedTextStart.equals(latestCommittedTextEnd))) { try { int startIndex = latestCommittedTextStart.getOffset(); int endIndex = latestCommittedTextEnd.getOffset(); String latestCommittedText = doc.getText(startIndex, endIndex - startIndex); doc.remove(startIndex, endIndex - startIndex); return new AttributedString(latestCommittedText).getIterator(); } catch (BadLocationException ble) {} } return null; } public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, Attribute[] attributes) { int composedStartIndex = 0; int composedEndIndex = 0; if (composedTextExists()) { composedStartIndex = composedTextStart.getOffset(); composedEndIndex = composedTextEnd.getOffset(); } String committed; try { if (beginIndex < composedStartIndex) { if (endIndex <= composedStartIndex) { committed = getText(beginIndex, endIndex - beginIndex); } else { int firstPartLength = composedStartIndex - beginIndex; committed = getText(beginIndex, firstPartLength) + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); } } else { committed = getText(beginIndex + (composedEndIndex - composedStartIndex), endIndex - beginIndex); } } catch (BadLocationException ble) { throw new IllegalArgumentException("Invalid range"); } return new AttributedString(committed).getIterator(); } public int getCommittedTextLength() { Document doc = getDocument(); int length = 0; if (doc != null) { length = doc.getLength(); if (composedTextContent != null) { if (composedTextEnd == null || composedTextStart == null) { /* * fix for : 6355666 * this is the case when this method is invoked * from DocumentListener. At this point * composedTextEnd and composedTextStart are * not defined yet. */ length -= composedTextContent.length(); } else { length -= composedTextEnd.getOffset() - composedTextStart.getOffset(); } } } return length; } public int getInsertPositionOffset() { int composedStartIndex = 0; int composedEndIndex = 0; if (composedTextExists()) { composedStartIndex = composedTextStart.getOffset(); composedEndIndex = composedTextEnd.getOffset(); } int caretIndex = getCaretPosition(); if (caretIndex < composedStartIndex) { return caretIndex; } else if (caretIndex < composedEndIndex) { return composedStartIndex; } else { return caretIndex - (composedEndIndex - composedStartIndex); } } public TextHitInfo getLocationOffset(int x, int y) { if (composedTextAttribute == null) { return null; } else { Point p = getLocationOnScreen(); p.x = x - p.x; p.y = y - p.y; int pos = viewToModel(p); if ((pos >= composedTextStart.getOffset()) && (pos <= composedTextEnd.getOffset())) { return TextHitInfo.leading(pos - composedTextStart.getOffset()); } else { return null; } } } public Rectangle getTextLocation(TextHitInfo offset) { Rectangle r; try { r = modelToView(getCaretPosition()); if (r != null) { Point p = getLocationOnScreen(); r.translate(p.x, p.y); } } catch (BadLocationException ble) { r = null; } if (r == null) r = new Rectangle(); return r; } public AttributedCharacterIterator getSelectedText( Attribute[] attributes) { String selection = JTextComponent.this.getSelectedText(); if (selection != null) { return new AttributedString(selection).getIterator(); } else { return null; } } // --- DocumentListener methods --- public void changedUpdate(DocumentEvent e) { latestCommittedTextStart = latestCommittedTextEnd = null; } public void insertUpdate(DocumentEvent e) { latestCommittedTextStart = latestCommittedTextEnd = null; } public void removeUpdate(DocumentEvent e) { latestCommittedTextStart = latestCommittedTextEnd = null; } } // // Replaces the current input method (composed) text according to // the passed input method event. This method also inserts the // committed text into the document. // private void replaceInputMethodText(InputMethodEvent e) { int commitCount = e.getCommittedCharacterCount(); AttributedCharacterIterator text = e.getText(); int composedTextIndex; // old composed text deletion Document doc = getDocument(); if (composedTextExists()) { try { doc.remove(composedTextStart.getOffset(), composedTextEnd.getOffset() - composedTextStart.getOffset()); } catch (BadLocationException ble) {} composedTextStart = composedTextEnd = null; composedTextAttribute = null; composedTextContent = null; } if (text != null) { text.first(); int committedTextStartIndex = 0; int committedTextEndIndex = 0; // committed text insertion if (commitCount > 0) { // Remember latest committed text start index committedTextStartIndex = caret.getDot(); // Need to generate KeyTyped events for the committed text for components // that are not aware they are active input method clients. if (shouldSynthensizeKeyEvents()) { for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { KeyEvent ke = new KeyEvent(this, KeyEvent.KEY_TYPED, EventQueue.getMostRecentEventTime(), 0, KeyEvent.VK_UNDEFINED, c); processKeyEvent(ke); } } else { StringBuilder strBuf = new StringBuilder(); for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { strBuf.append(c); } // map it to an ActionEvent mapCommittedTextToAction(strBuf.toString()); } // Remember latest committed text end index committedTextEndIndex = caret.getDot(); } // new composed text insertion composedTextIndex = text.getIndex(); if (composedTextIndex < text.getEndIndex()) { createComposedTextAttribute(composedTextIndex, text); try { replaceSelection(null); doc.insertString(caret.getDot(), composedTextContent, composedTextAttribute); composedTextStart = doc.createPosition(caret.getDot() - composedTextContent.length()); composedTextEnd = doc.createPosition(caret.getDot()); } catch (BadLocationException ble) { composedTextStart = composedTextEnd = null; composedTextAttribute = null; composedTextContent = null; } } // Save the latest committed text information if (committedTextStartIndex != committedTextEndIndex) { try { latestCommittedTextStart = doc. createPosition(committedTextStartIndex); latestCommittedTextEnd = doc. createPosition(committedTextEndIndex); } catch (BadLocationException ble) { latestCommittedTextStart = latestCommittedTextEnd = null; } } else { latestCommittedTextStart = latestCommittedTextEnd = null; } } } private void createComposedTextAttribute(int composedIndex, AttributedCharacterIterator text) { Document doc = getDocument(); StringBuilder strBuf = new StringBuilder(); // create attributed string with no attributes for (char c = text.setIndex(composedIndex); c != CharacterIterator.DONE; c = text.next()) { strBuf.append(c); } composedTextContent = strBuf.toString(); composedTextAttribute = new SimpleAttributeSet(); composedTextAttribute.addAttribute(StyleConstants.ComposedTextAttribute, new AttributedString(text, composedIndex, text.getEndIndex())); } /** * Saves composed text around the specified position. * * The composed text (if any) around the specified position is saved * in a backing store and removed from the document. * * @param pos document position to identify the composed text location * @return {@code true} if the composed text exists and is saved, * {@code false} otherwise * @see #restoreComposedText * @since 1.7 */ protected boolean saveComposedText(int pos) { if (composedTextExists()) { int start = composedTextStart.getOffset(); int len = composedTextEnd.getOffset() - composedTextStart.getOffset(); if (pos >= start && pos <= start + len) { try { getDocument().remove(start, len); return true; } catch (BadLocationException ble) {} } } return false; } /** * Restores composed text previously saved by {@code saveComposedText}. * * The saved composed text is inserted back into the document. This method * should be invoked only if {@code saveComposedText} returns {@code true}. * * @see #saveComposedText * @since 1.7 */ protected void restoreComposedText() { Document doc = getDocument(); try { doc.insertString(caret.getDot(), composedTextContent, composedTextAttribute); composedTextStart = doc.createPosition(caret.getDot() - composedTextContent.length()); composedTextEnd = doc.createPosition(caret.getDot()); } catch (BadLocationException ble) {} } // // Map committed text to an ActionEvent. If the committed text length is 1, // treat it as a KeyStroke, otherwise or there is no KeyStroke defined, // treat it just as a default action. // private void mapCommittedTextToAction(String committedText) { Keymap binding = getKeymap(); if (binding != null) { Action a = null; if (committedText.length() == 1) { KeyStroke k = KeyStroke.getKeyStroke(committedText.charAt(0)); a = binding.getAction(k); } if (a == null) { a = binding.getDefaultAction(); } if (a != null) { ActionEvent ae = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, committedText, EventQueue.getMostRecentEventTime(), getCurrentEventModifiers()); a.actionPerformed(ae); } } } // // Sets the caret position according to the passed input method // event. Also, sets/resets composed text caret appropriately. // private void setInputMethodCaretPosition(InputMethodEvent e) { int dot; if (composedTextExists()) { dot = composedTextStart.getOffset(); if (!(caret instanceof ComposedTextCaret)) { if (composedTextCaret == null) { composedTextCaret = new ComposedTextCaret(); } originalCaret = caret; // Sets composed text caret exchangeCaret(originalCaret, composedTextCaret); } TextHitInfo caretPos = e.getCaret(); if (caretPos != null) { int index = caretPos.getInsertionIndex(); dot += index; if (index == 0) { // Scroll the component if needed so that the composed text // becomes visible. try { Rectangle d = modelToView(dot); Rectangle end = modelToView(composedTextEnd.getOffset()); Rectangle b = getBounds(); d.x += Math.min(end.x - d.x, b.width); scrollRectToVisible(d); } catch (BadLocationException ble) {} } } caret.setDot(dot); } else if (caret instanceof ComposedTextCaret) { dot = caret.getDot(); // Restores original caret exchangeCaret(caret, originalCaret); caret.setDot(dot); } } private void exchangeCaret(Caret oldCaret, Caret newCaret) { int blinkRate = oldCaret.getBlinkRate(); setCaret(newCaret); caret.setBlinkRate(blinkRate); caret.setVisible(hasFocus()); } /** * Returns true if KeyEvents should be synthesized from an InputEvent. */ private boolean shouldSynthensizeKeyEvents() { if (!checkedInputOverride) { checkedInputOverride = true; needToSendKeyTypedEvent = !isProcessInputMethodEventOverridden(); } return needToSendKeyTypedEvent; } // // 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. // private boolean isProcessInputMethodEventOverridden() { if (overrideMap == null) { overrideMap = Collections.synchronizedMap(new HashMap()); } Boolean retValue = overrideMap.get(getClass().getName()); if (retValue != null) { return retValue.booleanValue(); } Boolean ret = AccessController.doPrivileged(new PrivilegedAction() { public Boolean run() { return isProcessInputMethodEventOverridden( JTextComponent.this.getClass()); } }); return ret.booleanValue(); } // // 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()); } } }