diff --git a/app/src/processing/app/syntax/im/CompositionTextManager.java b/app/src/processing/app/syntax/im/CompositionTextManager.java index 12a9ad341..04fbe5f74 100644 --- a/app/src/processing/app/syntax/im/CompositionTextManager.java +++ b/app/src/processing/app/syntax/im/CompositionTextManager.java @@ -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() { diff --git a/app/src/processing/app/syntax/im/CompositionTextPainter.java b/app/src/processing/app/syntax/im/CompositionTextPainter.java index c12d7ed30..bb0e43e98 100644 --- a/app/src/processing/app/syntax/im/CompositionTextPainter.java +++ b/app/src/processing/app/syntax/im/CompositionTextPainter.java @@ -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; } } diff --git a/app/src/processing/app/syntax/im/InputMethodSupport.java b/app/src/processing/app/syntax/im/InputMethodSupport.java index 461be3d16..921cfc83b 100644 --- a/app/src/processing/app/syntax/im/InputMethodSupport.java +++ b/app/src/processing/app/syntax/im/InputMethodSupport.java @@ -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 Bug 854 : implement input method support for Japanese (and other languages) + * @see Bug 1531 : Can't input full-width space when Japanese IME is on. + * @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(); + } + } }