/* * @(#)JTextComponent.java 1.212 04/04/15 * * Copyright 2004 Sun Microsystems, Inc. All rights reserved. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. */ package javax.swing.text; import java.lang.reflect.Method; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Collections; import java.util.HashMap; import java.util.Hashtable; import java.util.Enumeration; import java.util.Vector; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.io.*; import java.awt.*; import java.awt.event.*; 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.text.*; import java.text.AttributedCharacterIterator.Attribute; import javax.swing.*; import javax.swing.event.*; import javax.swing.plaf.*; import javax.accessibility.*; import sun.awt.AppContext; /** * 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. *
* *

* 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 * @version 1.212 04/15/04 * @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 (CaretListener[])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; 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 */ 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); } /** * Sets the dragEnabled property, * which must be true to enable * automatic drag handling (the first part of drag and drop) * on this component. * The transferHandler property needs to be set * to a non-null value for the drag to do * anything. The default value of the dragEnabled * property * is false. *

* When automatic drag handling is enabled, * most look and feels 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 true * can therefore have a subtle effect on * how selections behave. *

* Some look and feels might not support automatic drag and drop; * they will ignore this property. You can work around such * look and feels by modifying the component * to directly call the exportAsDrag method of a * TransferHandler. * * @param b the value to set the dragEnabled property to * @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; } /** * Gets the dragEnabled property. * * @return the value of the dragEnabled property * @see #setDragEnabled * @since 1.4 */ public boolean getDragEnabled() { return dragEnabled; } /** * 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() { 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 (int i = 0; i < actions.length; i++) { Action a = actions[i]; String value = (String)a.getValue(Action.NAME); h.put((value!=null ? value:""), a); } for (int i = 0; i < bindings.length; i++) { Action a = (Action) h.get(bindings[i].actionName); if (a != null) { map.addActionForKeyStroke(bindings[i].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 = (Boolean)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 * selected 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. *

* This method is thread safe, although most Swing methods * are not. Please see * Threads * and Swing for more information. * * @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 */ 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. *

* This method is thread safe, although most Swing methods * are not. Please see * Threads * and Swing for more information. * * 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; if (editable) { setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); } else { setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } 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 */ 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 */ 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() { if (getParent() instanceof JViewport) { return (((JViewport)getParent()).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() { if (getParent() instanceof JViewport) { return (((JViewport)getParent()).getHeight() > getPreferredSize().height); } return false; } ///////////////// // 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 { 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 = null; 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 */ public AccessibleEditableText getAccessibleEditableText() { return this; } /** * Sets the text contents to the specified string. * * @param s the string to set the text contents */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 // --- 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 */ 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 */ 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 */ 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; /** * 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 (int counter = 0; counter < flavors.length; counter++) { if (flavors[counter].equals(DataFlavor.stringFlavor)) { return flavors[counter]; } } } 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"); private JTextComponent editor; // // 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 = (Action) 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++] = (KeyStroke) 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++] = (Action) 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 enum_ = bindings.keys(); enum_.hasMoreElements();) { Object key = enum_.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 = (InputMethodRequests)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) { 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 { StringBuffer strBuf = new StringBuffer(); for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { strBuf.append(c); } // map it to an ActionEvent mapCommittedTextToAction(new String(strBuf)); } // 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(); StringBuffer strBuf = new StringBuffer(); // create attributed string with no attributes for (char c = text.setIndex(composedIndex); c != CharacterIterator.DONE; c = text.next()) { strBuf.append(c); } composedTextContent = new String(strBuf); composedTextAttribute = new SimpleAttributeSet(); composedTextAttribute.addAttribute(StyleConstants.ComposedTextAttribute, new AttributedString(text, composedIndex, text.getEndIndex())); } private 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; } private 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 = (Boolean)overrideMap.get(getClass().getName()); if (retValue != null) { return retValue.booleanValue(); } Boolean ret = (Boolean)AccessController.doPrivileged(new PrivilegedAction() { public Object 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()); } } }