package processing.app.syntax.im;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.InputMethodEvent;
import java.awt.event.InputMethodListener;
import java.awt.font.FontRenderContext;
import java.awt.font.TextAttribute;
import java.awt.font.TextHitInfo;
import java.awt.font.TextLayout;
import java.awt.im.InputMethodRequests;
import java.text.AttributedCharacterIterator;
import java.text.AttributedCharacterIterator.Attribute;
import java.text.AttributedString;
import processing.app.Base;
import processing.app.Messages;
import processing.app.Preferences;
import processing.app.syntax.JEditTextArea;
import processing.app.syntax.TextAreaDefaults;
import processing.app.syntax.TextAreaPainter;
/**
* On-the-spot style input support for CJK (Chinese, Japanese, Korean).
*
* @see <a href="https://processing.org/bugs/bugzilla/854.html">Bugzilla 854: implement input method support for Japanese (and other languages)</a>
* @see <a href="https://processing.org/bugs/bugzilla/1531.html">Bugzilla 1531: Can't input full-width space when Japanese IME is on.</a>
* @see <a href="http://docs.oracle.com/javase/8/docs/technotes/guides/imf/index.html">Java Input Method Framework (IMF) Technology</a>
* @see <a href="http://docs.oracle.com/javase/tutorial/2d/text/index.html">The Java Tutorials</a>
*
* @author Takashi Maekawa (takachin@generative.info)
* @author Satoshi Okita
*/
public class InputMethodSupport implements InputMethodRequests, InputMethodListener {
/*
public interface Callback {
public void onCommitted(char c);
}
private Callback callback;
*/
static private final Attribute[] CUSTOM_IM_ATTRIBUTES = {
TextAttribute.INPUT_METHOD_HIGHLIGHT,
};
private JEditTextArea textArea;
private int committedCount = 0;
private AttributedString composedTextString;
public InputMethodSupport(JEditTextArea textArea) {
this.textArea = textArea;
textArea.enableInputMethods(true);
textArea.addInputMethodListener(this);
}
/*
public void setCallback(Callback callback) {
this.callback = callback;
}
*/
/////////////////////////////////////////////////////////////////////////////
// InputMethodRequest
/////////////////////////////////////////////////////////////////////////////
@Override
public Rectangle getTextLocation(TextHitInfo offset) {
if (Base.DEBUG) {
Messages.log("#Called getTextLocation:" + offset);
}
int line = textArea.getCaretLine();
int offsetX = textArea.getCaretPosition() - textArea.getLineStartOffset(line);
// '+1' mean textArea.lineToY(line) + textArea.getPainter().getFontMetrics().getHeight().
// TextLayout#draw method need at least one height of font.
Rectangle rectangle = new Rectangle(textArea.offsetToX(line, offsetX), textArea.lineToY(line + 1), 0, 0);
Point location = textArea.getPainter().getLocationOnScreen();
rectangle.translate(location.x, location.y);
return rectangle;
}
@Override
public TextHitInfo getLocationOffset(int x, int y) {
return null;
}
@Override
public int getInsertPositionOffset() {
return -textArea.getCaretPosition();
}
@Override
public AttributedCharacterIterator getCommittedText(int beginIndex,
int endIndex, AttributedCharacterIterator.Attribute[] attributes) {
int length = endIndex - beginIndex;
String textAreaString = textArea.getText(beginIndex, length);
return new AttributedString(textAreaString).getIterator();
}
@Override
public int getCommittedTextLength() {
return committedCount;
}
@Override
public AttributedCharacterIterator cancelLatestCommittedText(
AttributedCharacterIterator.Attribute[] attributes) {
return null;
}
@Override
public AttributedCharacterIterator getSelectedText(
AttributedCharacterIterator.Attribute[] attributes) {
return null;
}
/////////////////////////////////////////////////////////////////////////////
// InputMethodListener
/////////////////////////////////////////////////////////////////////////////
/**
* Handles events from InputMethod.
*
* @param event event from Input Method.
*/
@Override
public void inputMethodTextChanged(InputMethodEvent event) {
if (Base.DEBUG) {
StringBuilder sb = new StringBuilder();
sb.append("#Called inputMethodTextChanged");
sb.append("\t ID: " + event.getID());
sb.append("\t timestamp: " + new java.util.Date(event.getWhen()));
sb.append("\t parmString: " + event.paramString());
Messages.log(sb.toString());
}
AttributedCharacterIterator text = event.getText(); // text = composedText + commitedText
committedCount = event.getCommittedCharacterCount();
// The caret for Input Method. If you type a character by a input method,
// original caret position will be incorrect. JEditTextArea is not
// implemented using AttributedString and TextLayout.
textArea.setCaretVisible(false);
// Japanese : if the enter key pressed, event.getText is null.
// Japanese : if first space key pressed, event.getText is null.
// Chinese (pinin) : if a space key pressed, event.getText is null.
// Taiwan (bopomofo): ?
// Korean : ?
// Korean Input Method
if (text != null && text.getEndIndex() - (text.getBeginIndex() + committedCount) <= 0) {
textArea.setCaretVisible(true);
}
// Japanese Input Method
if (text == null) {
textArea.setCaretVisible(true);
}
if (text != null) {
if (committedCount > 0) {
char[] insertion = new char[committedCount];
char c = text.first();
for (int i = 0; i < committedCount; i++) {
insertion[i] = c;
c = text.next();
}
// Insert this as a compound edit
textArea.setSelectedText(new String(insertion), true);
textArea.getInputHandler().handleInputMethodCommit();
}
CompositionTextPainter compositionPainter = textArea.getPainter().getCompositionTextpainter();
Messages.log("textArea.getCaretPosition() + committed_count: " + (textArea.getCaretPosition() + committedCount));
compositionPainter.setComposedTextLayout(getTextLayout(text, committedCount), textArea.getCaretPosition() + committedCount);
compositionPainter.setCaret(event.getCaret());
} else { // otherwise hide the input method
CompositionTextPainter compositionPainter = textArea.getPainter().getCompositionTextpainter();
compositionPainter.setComposedTextLayout(null, 0);
compositionPainter.setCaret(null);
}
event.consume();
textArea.repaint();
}
private TextLayout getTextLayout(AttributedCharacterIterator text, int committedCount) {
boolean antialias = Preferences.getBoolean("editor.smooth");
TextAreaPainter painter = textArea.getPainter();
// create attributed string with font info.
if (text.getEndIndex() - (text.getBeginIndex() + committedCount) > 0) {
composedTextString = new AttributedString(text, committedCount, text.getEndIndex(), CUSTOM_IM_ATTRIBUTES);
Font font = painter.getFontMetrics().getFont();
TextAreaDefaults defaults = textArea.getDefaults();
Color bgColor = defaults.lineHighlight ?
defaults.lineHighlightColor : defaults.bgcolor;
composedTextString.addAttribute(TextAttribute.FONT, font);
composedTextString.addAttribute(TextAttribute.FOREGROUND, defaults.fgcolor);
composedTextString.addAttribute(TextAttribute.BACKGROUND, bgColor);
} else {
composedTextString = new AttributedString("");
return null;
}
// set hint of antialiasing to render target.
Graphics2D g2d = (Graphics2D)painter.getGraphics();
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
antialias ?
RenderingHints.VALUE_TEXT_ANTIALIAS_ON :
RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
FontRenderContext frc = g2d.getFontRenderContext();
if (Base.DEBUG) {
Messages.log("debug: FontRenderContext is Antialiased = " + frc.getAntiAliasingHint());
}
return new TextLayout(composedTextString.getIterator(), frc);
}
@Override
public void caretPositionChanged(InputMethodEvent event) {
event.consume();
}
/*
private void insertCharacter(char c) {
if (Base.DEBUG) {
Messages.log("debug: insertCharacter(char c) textArea.getCaretPosition()=" + textArea.getCaretPosition());
}
try {
textArea.getDocument().insertString(textArea.getCaretPosition(), Character.toString(c), null);
if (Base.DEBUG) {
Messages.log("debug: \t after:insertCharacter(char c) textArea.getCaretPosition()=" + textArea.getCaretPosition());
}
} catch (BadLocationException e) {
e.printStackTrace();
}
}
*/
}