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