Merge pull request #4293 from satoshiokita/FIX-InputMethodCJKSupport

Fixed InputMethod Chiese/Japanese/Korean support.
This commit is contained in:
Ben Fry
2016-02-13 20:11:02 -05:00
3 changed files with 264 additions and 92 deletions

View File

@@ -5,6 +5,7 @@ import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
@@ -14,6 +15,9 @@ import java.text.CharacterIterator;
import javax.swing.text.BadLocationException;
import processing.app.Base;
import processing.app.Messages;
import processing.app.Preferences;
import processing.app.syntax.JEditTextArea;
import processing.app.syntax.TextAreaPainter;
@@ -152,14 +156,28 @@ public class CompositionTextManager {
e.printStackTrace();
}
}
private TextLayout getTextLayout(AttributedCharacterIterator text, int committed_count) {
AttributedString composed = new AttributedString(text, committed_count, text.getEndIndex());
Font font = textArea.getPainter().getFont();
FontRenderContext context = ((Graphics2D) (textArea.getPainter().getGraphics())).getFontRenderContext();
private TextLayout getTextLayout(AttributedCharacterIterator text, int committedCount) {
boolean antialias = Preferences.getBoolean("editor.smooth");
TextAreaPainter painter = textArea.getPainter();
// create attributed string with font info.
AttributedString composed = new AttributedString(text, committedCount, text.getEndIndex());
Font font = painter.getFontMetrics().getFont();
composed.addAttribute(TextAttribute.FONT, font);
TextLayout layout = new TextLayout(composed.getIterator(), context);
return layout;
// 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(composed.getIterator(), frc);
}
private Point getCaretLocation() {

View File

@@ -1,12 +1,15 @@
package processing.app.syntax.im;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.font.TextHitInfo;
import java.awt.font.TextLayout;
import processing.app.Base;
import processing.app.Messages;
import processing.app.syntax.JEditTextArea;
/**
@@ -24,8 +27,9 @@ public class CompositionTextPainter {
private TextLayout composedTextLayout;
private int composedBeginCaretPosition = 0;
private JEditTextArea textArea;
private boolean caretColorFlag;
private TextHitInfo caret;
/**
* Constructor for painter.
* @param textArea textarea used by PDE.
@@ -83,45 +87,76 @@ public class CompositionTextPainter {
*
* This draw method is proceeded with the following steps.
* 1. Original TextAreaPainter draws characters.
* 2. This refillComposedArea method erase previous paint characters by textarea's background color.
* The refill area is only square that width and height defined by characters with input method.
* 3. CompositionTextPainter.draw method paints composed text. It was actually drawn by TextLayout.
* 2. CompositionTextPainter.draw method paints composed text. It was actually drawn by TextLayout.
*
* @param gfx set TextAreaPainter's Graphics object.
* @param fillBackGroundColor set textarea's background.
*/
// public void draw(Graphics gfx, Color fillBackGroundColor) {
// assert(composedTextLayout != null);
// Point composedLoc = getCaretLocation();
// //refillComposedArea(fillBackGroundColor, composedLoc.x, composedLoc.y);
// composedTextLayout.draw((Graphics2D) gfx, composedLoc.x, composedLoc.y);
// }
public void draw(Graphics gfx, Color fillBackGroundColor) {
assert(composedTextLayout != null);
Point composedLoc = getCaretLocation();
refillComposedArea(fillBackGroundColor, composedLoc.x, composedLoc.y);
composedTextLayout.draw((Graphics2D) gfx, composedLoc.x, composedLoc.y);
//assert(composedTextLayout != null);
if (composedTextLayout != null) {
Point composedLoc = getCaretLocation();
//refillComposedArea(fillBackGroundColor, composedLoc.x, composedLoc.y);
composedTextLayout.draw((Graphics2D) gfx, composedLoc.x, composedLoc.y);
// draw caret.
if (this.caret != null) {
int caretLocation = Math.round(composedTextLayout.getCaretInfo(caret)[0]);
Rectangle rect = new Rectangle(composedLoc.x + caretLocation,
composedLoc.y - (int)composedTextLayout.getAscent(),
1,
(int)composedTextLayout.getAscent() + (int)composedTextLayout.getDescent());
// blink caret.
if (Base.DEBUG) {
Messages.log(rect.toString());
}
Color c = gfx.getColor(); // save
if (caretColorFlag) {
caretColorFlag = false;
gfx.setColor(Color.WHITE);
} else {
caretColorFlag = true;
gfx.setColor(Color.BLACK);
}
gfx.fillRect(rect.x, rect.y, rect.width, rect.height);
gfx.setColor(c); // restore
}
}
}
/**
* Fill color to erase characters drawn by original TextAreaPainter.
*
* @param fillColor fill color to erase characters drawn by original TextAreaPainter method.
* @param x x-coordinate where to fill.
* @param y y-coordinate where to fill.
*/
private void refillComposedArea(Color fillColor, int x, int y) {
Graphics gfx = textArea.getPainter().getGraphics();
gfx.setColor(fillColor);
FontMetrics fm = textArea.getPainter().getFontMetrics();
int newY = y - (fm.getHeight() - CompositionTextManager.COMPOSING_UNDERBAR_HEIGHT);
int paintHeight = fm.getHeight();
int paintWidth = (int) composedTextLayout.getBounds().getWidth();
gfx.fillRect(x, newY, paintWidth, paintHeight);
}
// /**
// * Fill color to erase characters drawn by original TextAreaPainter.
// *
// * @param fillColor fill color to erase characters drawn by original TextAreaPainter method.
// * @param x x-coordinate where to fill.
// * @param y y-coordinate where to fill.
// */
// private void refillComposedArea(Color fillColor, int x, int y) {
// Graphics gfx = textArea.getPainter().getGraphics();
// gfx.setColor(fillColor);
// FontMetrics fm = textArea.getPainter().getFontMetrics();
// int newY = y - (fm.getHeight() - CompositionTextManager.COMPOSING_UNDERBAR_HEIGHT);
// int paintHeight = fm.getHeight();
// int paintWidth = (int) composedTextLayout.getBounds().getWidth();
// gfx.fillRect(x, newY, paintWidth, paintHeight);
// }
private Point getCaretLocation() {
FontMetrics fm = textArea.getPainter().getFontMetrics();
int offsetY = fm.getHeight() - CompositionTextManager.COMPOSING_UNDERBAR_HEIGHT;
int lineIndex = textArea.getCaretLine();
int offsetX = composedBeginCaretPosition - textArea.getLineStartOffset(lineIndex);
return new Point(textArea.offsetToX(lineIndex, offsetX),
(lineIndex - textArea.getFirstLine()) * fm.getHeight() + offsetY);
int line = textArea.getCaretLine();
int offsetX = composedBeginCaretPosition - textArea.getLineStartOffset(line);
// '+1' mean textArea.lineToY(line) + textArea.getPainter().getFontMetrics().getHeight().
// TextLayout#draw method need at least one height of font.
return new Point(textArea.offsetToX(line, offsetX), textArea.lineToY(line + 1));
}
public void setCaret(TextHitInfo caret) {
this.caret = caret;
}
}

View File

@@ -1,120 +1,239 @@
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 javax.swing.text.BadLocationException;
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.TextAreaPainter;
/**
* Support in-line Japanese input for PDE. (Maybe Chinese, Korean and more)
* This class is implemented by Java Input Method Framework and handles
* If you would like to know more about Java Input Method Framework,
* Please see http://java.sun.com/j2se/1.5.0/docs/guide/imf/
*
* This class is implemented to fix Bug #854.
* http://dev.processing.org/bugs/show_bug.cgi?id=854
* on-the-spot style input support for CJK.(Chinese, Japanese, Korean).
* This class is implemented to fix Bug #854 from 2010-02-16.
*
* @see <a href="https://processing.org/bugs/bugzilla/854.html">Bug 854 : implement input method support for Japanese (and other languages)</a>
* @see <a href="https://processing.org/bugs/bugzilla/1531.html">Bug 1531 : Can't input full-width space when Japanese IME is on.</a>
* @see http://docs.oracle.com/javase/8/docs/technotes/guides/imf/index.html
* @see http://docs.oracle.com/javase/tutorial/2d/text/index.html
*
* @author Takashi Maekawa (takachin@generative.info)
* @author Satoshi Okita
*/
public class InputMethodSupport implements InputMethodRequests,
InputMethodListener {
private static final Attribute[] CUSTOM_IM_ATTRIBUTES = {
TextAttribute.INPUT_METHOD_HIGHLIGHT,
};
private int committed_count = 0;
private CompositionTextManager textManager;
private TextHitInfo caret;
private JEditTextArea textArea;
private AttributedCharacterIterator composedText;
private AttributedString composedTextString;
public InputMethodSupport(JEditTextArea textArea) {
textManager = new CompositionTextManager(textArea);
textArea.enableInputMethods(true);
textArea.addInputMethodListener(this);
this.textArea = textArea;
this.textArea.enableInputMethods(true);
this.textArea.addInputMethodListener(this);
}
/////////////////////////////////////////////////////////////////////////////
// InputMethodRequest
/////////////////////////////////////////////////////////////////////////////
@Override
public Rectangle getTextLocation(TextHitInfo offset) {
return textManager.getTextLocation();
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 textManager.getInsertPositionOffset();
return textArea.getCaretPosition() * -1;
}
@Override
public AttributedCharacterIterator getCommittedText(int beginIndex,
int endIndex, AttributedCharacterIterator.Attribute[] attributes) {
return textManager.getCommittedText(beginIndex, endIndex);
int length = endIndex - beginIndex;
String textAreaString = textArea.getText(beginIndex, length);
return new AttributedString(textAreaString).getIterator();
}
@Override
public int getCommittedTextLength() {
return committed_count;
}
@Override
public AttributedCharacterIterator cancelLatestCommittedText(
AttributedCharacterIterator.Attribute[] attributes) {
return null;
}
@Override
public AttributedCharacterIterator getSelectedText(
AttributedCharacterIterator.Attribute[] attributes) {
return null;
}
/////////////////////////////////////////////////////////////////////////////
// InputMethodListener
/////////////////////////////////////////////////////////////////////////////
/**
* Handles events from InputMethod.
* This method judges whether beginning of input or
* progress of input or end and call related method.
*
* @param event event from Input Method.
*/
public void inputMethodTextChanged(InputMethodEvent event) {
AttributedCharacterIterator text = event.getText();
@Override
public void inputMethodTextChanged(InputMethodEvent event) {
composedText = null;
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
committed_count = event.getCommittedCharacterCount();
if(isFullWidthSpaceInput(text)){
textManager.insertFullWidthSpace();
caretPositionChanged(event);
return;
// The caret for Input Method.
// if you type a character by a input method, original caret become off.
// a JEditTextArea is not implemented by the AttributedStirng and TextLayout.
// so JEditTextArea Caret On-off logic.
//
// 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 : ?
textArea.setCaretVisible(false);
// Korean Input Method
if (text != null && text.getEndIndex() - (text.getBeginIndex() + committed_count) <= 0) {
textArea.setCaretVisible(true);
}
if(isBeginInputProcess(text, textManager)){
textManager.beginCompositionText(text, committed_count);
caretPositionChanged(event);
return;
// Japanese Input Method
if (text == null) {
textArea.setCaretVisible(true);
}
if (isInputProcess(text)){
textManager.processCompositionText(text, committed_count);
caretPositionChanged(event);
return;
char c;
if (text != null) {
int toCopy = committed_count;
c = text.first();
while (toCopy-- > 0) {
if (Base.DEBUG) {
Messages.log("INSERT:'" + c + "'");
}
this.insertCharacter(c);
c = text.next();
}
CompositionTextPainter compositionPainter = textArea.getPainter().getCompositionTextpainter();
if (Base.DEBUG) {
Messages.log(" textArea.getCaretPosition() + committed_count: " + (textArea.getCaretPosition() + committed_count));
}
compositionPainter.setComposedTextLayout(getTextLayout(text, committed_count), textArea.getCaretPosition() + committed_count);
compositionPainter.setCaret(event.getCaret());
} else {
// hide input method.
CompositionTextPainter compositionPainter = textArea.getPainter().getCompositionTextpainter();
compositionPainter.setComposedTextLayout(null, 0);
compositionPainter.setCaret(null);
}
textManager.endCompositionText(text, committed_count);
caretPositionChanged(event);
caret = event.getCaret();
event.consume();
textArea.repaint();
}
private boolean isFullWidthSpaceInput(AttributedCharacterIterator text){
if(text == null)
return false;
if(textManager.getIsInputProcess())
return false;
return (String.valueOf(text.first()).equals("\u3000"));
}
private boolean isBeginInputProcess(AttributedCharacterIterator text, CompositionTextManager textManager){
if(text == null)
return false;
if(textManager.getIsInputProcess())
return false;
return (isInputProcess(text));
}
private boolean isInputProcess(AttributedCharacterIterator text){
if(text == null)
return false;
return (text.getEndIndex() - (text.getBeginIndex() + committed_count) > 0);
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() + committedCharacterCount) > 0) {
if (text.getEndIndex() - (text.getBeginIndex() + committedCount) > 0) {
composedTextString = new AttributedString(text, committedCount, text.getEndIndex(), CUSTOM_IM_ATTRIBUTES);
Font font = painter.getFontMetrics().getFont();
composedTextString.addAttribute(TextAttribute.FONT, font);
composedTextString.addAttribute(TextAttribute.BACKGROUND, Color.WHITE);
composedText = composedTextString.getIterator();
} 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) {
caret = event.getCaret();
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();
}
}
}