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