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(); } } */ }