/* * @(#)ActiveClient.java 1.2 99/08/02 * * Copyright 1997-1999 by Sun Microsystems, Inc., * 901 San Antonio Road, Palo Alto, California, 94303, U.S.A. * All rights reserved. */ import java.awt.Component; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.InputMethodEvent; import java.awt.event.InputMethodListener; import java.awt.im.InputMethodRequests; import java.awt.font.TextAttribute; import java.awt.font.TextHitInfo; import java.awt.font.TextLayout; import java.text.AttributedCharacterIterator; import java.text.AttributedCharacterIterator.Attribute; import java.text.AttributedString; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Implements an integrated text input user interface. * This class is an active client of the input method framework, that is, * actively uses its APIs to accomplish integration. *
* This class directly implements the two client interfaces of the input method * framework, InputMethodListener and InputMethodRequests. * This is not required. Especially in cases where the public * interface matters (such as in a class library), it may be more * appropriate to hide the implementations of these two interfaces * in separate classes. * */ public class ActiveClient extends LWTextComponent implements InputMethodListener, InputMethodRequests { // the composed text received from the input method // with font information added so we can easily create a TextLayout private AttributedString composedTextString = null; private AttributedCharacterIterator composedText = null; // the caret received from the input method; relative to composed text private TextHitInfo caret = null; /** * Constructs an ActiveClient. Input methods are always enabled for * ActiveClient instances. * @param name the component name to be displayed above the text */ public ActiveClient(String name) { super(name, true); enableInputMethods(true); addInputMethodListener(this); } /** * Implements getInputMethodRequests for ActiveClient by returning "this". * @return "this" */ public InputMethodRequests getInputMethodRequests() { return this; } /** * Adjusts composed text for new font size. */ public void setFontSize(int size) { super.setFontSize(size); if (composedTextString != null) { composedTextString.addAttribute(TextAttribute.FONT, getFont()); } } /** * Returns the text that the user has entered. * This override returns the concatenation of committed text * and composed text. */ public AttributedCharacterIterator getDisplayText() { if (composedText == null) { return super.getDisplayText(); } else { // We don't want to copy all the text and attribute data here. // Instead, we return a CompositeIterator which iterates over // the concatenation of two iterators. return new CompositeIterator(super.getDisplayText(), composedText); } } /** * Returns a text hit info indicating the current caret (insertion point). * This override returns the caret provided by the input method while * there is composed text; otherwise it returns a caret at the end * of the committed text. The caret provided by the input method may be null. */ public TextHitInfo getCaret() { if (composedText == null) { return super.getCaret(); } else if (caret == null) { return null; } else { // the caret provided by the input method is relative // to the composed text, so translate it to the entire text return caret.getOffsetHit(getCommittedTextLength()); } } // InputMethodListener implementation // constant attribute set for use when copying composed text private static final Attribute[] IM_ATTRIBUTES = { TextAttribute.INPUT_METHOD_HIGHLIGHT }; /** * Handles changes to the text entered through an input method. * Committed text contained in the event is appended to the * committed text of the text component. Composed text contained * in the event replaces any existing composed text in the text * component. * The caret defined in the event is saved and will * be returned by getCaret if there is composed text. The * component is redrawn. *
* In this simple component, we only * keep input method highlight attributes. Smarter components may want to * keep language, reading, input method segment, and other * attributes as well. */ public void inputMethodTextChanged(InputMethodEvent event) { int committedCharacterCount = event.getCommittedCharacterCount(); AttributedCharacterIterator text = event.getText(); composedText = null; char c; if (text != null) { // copy the committed text int toCopy = committedCharacterCount; c = text.first(); while (toCopy-- > 0) { insertCharacter(c); c = text.next(); } // copy the composed text if (text.getEndIndex() - (text.getBeginIndex() + committedCharacterCount) > 0) { composedTextString = new AttributedString(text, text.getBeginIndex() + committedCharacterCount, // skip over committed text text.getEndIndex(), IM_ATTRIBUTES); // add font information because TextLayout requires it composedTextString.addAttribute(TextAttribute.FONT, getFont()); composedText = composedTextString.getIterator(); } } event.consume(); invalidateTextLayout(); caret = event.getCaret(); repaint(); } /** * Handles changes to the caret within composed text. * The caret defined in the event is saved and will * be returned by getCaret if there is composed text. The * component is redrawn. */ public void caretPositionChanged(InputMethodEvent event) { caret = event.getCaret(); event.consume(); repaint(); } // InputMethodRequests implementation // Note that getCommittedTextLength is already implemented in LWTextComponent /** * Gets the location of a specified offset in the current composed text, * or of the selection in committed text. */ public Rectangle getTextLocation(TextHitInfo offset) { // determine the text location in component coordinates Rectangle rectangle; if (offset == null) { // no composed text: return caret for committed text rectangle = getCaretRectangle(); } else { // composed text: return caret within composed text TextHitInfo globalOffset = offset.getOffsetHit(getCommittedTextLength()); rectangle = getCaretRectangle(globalOffset); } // translate to screen coordinates Point location = getLocationOnScreen(); rectangle.translate(location.x, location.y); return rectangle; } /** * Gets the offset within the composed text for the specified absolute x * and y coordinates on the screen. */ public TextHitInfo getLocationOffset(int x, int y) { // translate from screen coordinates to coordinates in the text layout Point location = getLocationOnScreen(); Point textOrigin = getTextOrigin(); x -= location.x + textOrigin.x; y -= location.y + textOrigin.y; // TextLayout maps locations far outside its bounds to locations within. // To avoid false hits, we use it only if it actually contains the location. // We also have to translate the TextHitInfo to be relative to composed text. TextLayout textLayout = getTextLayout(); if (textLayout != null && textLayout.getBounds().contains(x, y)) { return textLayout.hitTestChar(x, y).getOffsetHit(-getCommittedTextLength()); } else { return null; } } /** * Gets the offset of the insert position in the committed text contained * in the text editing component. In this simple component, that's always * at the end of the committed text. */ public int getInsertPositionOffset() { return getCommittedTextLength(); } /** * Gets an iterator providing access to the entire text and attributes * contained in the text editing component except for uncommitted * text. */ public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, Attribute[] attributes) { return getCommittedText(beginIndex, endIndex); } /** * Returns null to indicate that the "Undo Commit" feature is not supported * by this simple text component. */ public AttributedCharacterIterator cancelLatestCommittedText(Attribute[] attributes) { return null; } private static final AttributedCharacterIterator EMPTY_TEXT = (new AttributedString("")).getIterator(); /** * Gets the currently selected text from the text editing component. * Since this simple text component doesn't support selections, this is * always an iterator over empty text. */ public AttributedCharacterIterator getSelectedText(Attribute[] attributes) { return EMPTY_TEXT; } } /** * Iterates over the combined text of two AttributedCharacterIterators. * Assumes that no annotation spans the two iterators. */ class CompositeIterator implements AttributedCharacterIterator { AttributedCharacterIterator iterator1; AttributedCharacterIterator iterator2; int begin1, end1; int begin2, end2; int endIndex; int currentIndex; AttributedCharacterIterator currentIterator; int currentIteratorDelta; /** * Constructs a CompositeIterator that iterates over the concatenation * of iterator1 and iterator2. * @param iterator1, iterator2 the base iterators that this composite iterator concatenates */ CompositeIterator(AttributedCharacterIterator iterator1, AttributedCharacterIterator iterator2) { this.iterator1 = iterator1; this.iterator2 = iterator2; begin1 = iterator1.getBeginIndex(); end1 = iterator1.getEndIndex(); begin2 = iterator2.getBeginIndex(); end2 = iterator2.getEndIndex(); endIndex = (end1 - begin1) + (end2 - begin2); internalSetIndex(0); } // CharacterIterator implementation public char first() { return internalSetIndex(0); } public char last() { if (endIndex == 0) { return internalSetIndex(endIndex); } else { return internalSetIndex(endIndex - 1); } } public char next() { if (currentIndex < endIndex) { return internalSetIndex(currentIndex + 1); } else { return DONE; } } public char previous() { if (currentIndex > 0) { return internalSetIndex(currentIndex - 1); } else { return DONE; } } public char current() { return currentIterator.setIndex(currentIndex + currentIteratorDelta); } public char setIndex(int position) { if (position < 0 || position > endIndex) { throw new IllegalArgumentException("invalid index"); } return internalSetIndex(position); } private char internalSetIndex(int position) { currentIndex = position; if (currentIndex < end1 - begin1) { currentIterator = iterator1; currentIteratorDelta = begin1; } else { currentIterator = iterator2; currentIteratorDelta = begin2 - (end1 - begin1); } return currentIterator.setIndex(currentIndex + currentIteratorDelta); } public int getBeginIndex() { return 0; } public int getEndIndex() { return endIndex; } public int getIndex() { return currentIndex; } // AttributedCharacterIterator implementation public int getRunStart() { return currentIterator.getRunStart() - currentIteratorDelta; } public int getRunLimit() { return currentIterator.getRunLimit() - currentIteratorDelta; } public int getRunStart(Attribute attribute) { return currentIterator.getRunStart(attribute) - currentIteratorDelta; } public int getRunLimit(Attribute attribute) { return currentIterator.getRunLimit(attribute) - currentIteratorDelta; } public int getRunStart(Set attributes) { return currentIterator.getRunStart(attributes) - currentIteratorDelta; } public int getRunLimit(Set attributes) { return currentIterator.getRunLimit(attributes) - currentIteratorDelta; } public Map getAttributes() { return currentIterator.getAttributes(); } public Set getAllAttributeKeys() { Set keys = new HashSet(iterator1.getAllAttributeKeys()); keys.addAll(iterator2.getAllAttributeKeys()); return keys; } public Object getAttribute(Attribute attribute) { return currentIterator.getAttribute(attribute); } // Object overrides public Object clone() { try { CompositeIterator other = (CompositeIterator) super.clone(); return other; } catch (CloneNotSupportedException e) { throw new InternalError(); } } }