Index: app/src/processing/app/syntax/InputMethodSupport.java =================================================================== --- app/src/processing/app/syntax/InputMethodSupport.java (revision 6115) +++ app/src/processing/app/syntax/InputMethodSupport.java (working copy) @@ -1,173 +0,0 @@ -package processing.app.syntax; - -import java.awt.Font; -import java.awt.FontMetrics; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Point; -import java.awt.Rectangle; -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.AttributedString; - -import javax.swing.text.BadLocationException; - -public class InputMethodSupport implements InputMethodRequests, - InputMethodListener { - - private JEditTextArea textArea; - - private TextLayout composedTextLayout = null; - - private int committed_count = 0; - - private static final int COMPOSING_UNDERBAR_HEIGHT = 5; - - private boolean isComposing; - - public InputMethodSupport(JEditTextArea textArea) { - this.textArea = textArea; - textArea.enableInputMethods(true); - textArea.addInputMethodListener(this); - isComposing = false; - } - - public boolean getIsComposing() { - return isComposing; - } - - private Point getCaretLocation() { - Point loc = new Point(); - TextAreaPainter painter = textArea.getPainter(); - FontMetrics fm = painter.getFontMetrics(); - int offsetY = fm.getHeight() - 5; - int lineIndex = textArea.getCaretLine(); - loc.y = lineIndex * fm.getHeight() + offsetY; - int offsetX = textArea.getCaretPosition() - - textArea.getLineStartOffset(lineIndex); - loc.x = textArea.offsetToX(lineIndex, offsetX); - return loc; - } - - public Rectangle getTextLocation(TextHitInfo offset) { - Point caret = getCaretLocation(); - return getCaretRectangle(caret.x, caret.y); - } - - private Rectangle getCaretRectangle(int x, int y) { - TextAreaPainter painter = textArea.getPainter(); - Point origin = painter.getLocationOnScreen(); - int height = painter.getFontMetrics().getHeight(); - return new Rectangle(origin.x + x, origin.y + y, 0, height); - } - - public TextHitInfo getLocationOffset(int x, int y) { - return null; - } - - public int getInsertPositionOffset() { - if (isComposing) { - isComposing = false; - } - return textArea.getCaretPosition(); - } - - public AttributedCharacterIterator getCommittedText(int beginIndex, - int endIndex, AttributedCharacterIterator.Attribute[] attributes) { - return (new AttributedString(textArea.getText(beginIndex, endIndex - - beginIndex))).getIterator(); - } - - public int getCommittedTextLength() { - return committed_count; - } - - public AttributedCharacterIterator cancelLatestCommittedText( - AttributedCharacterIterator.Attribute[] attributes) { - return null; - } - - public AttributedCharacterIterator getSelectedText( - AttributedCharacterIterator.Attribute[] attributes) { - return null; - } - - public void inputMethodTextChanged(InputMethodEvent event) { - composedTextLayout = null; - AttributedCharacterIterator text = event.getText(); - committed_count = event.getCommittedCharacterCount(); - if (committed_count == 0) { - if (text.getEndIndex() == 0) { - caretPositionChanged(event); - return; - } - if (text.getEndIndex() < text.getBeginIndex()) { - caretPositionChanged(event); - return; - } - isComposing = true; - drawComposingText(text, committed_count); - caretPositionChanged(event); - return; - } - commitText(text, committed_count); - isComposing = false; - caretPositionChanged(event); - } - - private void drawComposingText(AttributedCharacterIterator text, int count) { - assert ((count == 0 && text.getEndIndex() > 0)); - Point textLocation = getCaretLocation(); - invalidateComposingLine(textArea.getPainter(), textLocation.x, - textLocation.y); - composedTextLayout = getTextLayout(text, count); - composedTextLayout.draw((Graphics2D) (textArea.getPainter().getGraphics()), - textLocation.x, textLocation.y); - } - - private void invalidateComposingLine(TextAreaPainter painter, int x, int y) { - Graphics gfx = painter.getGraphics(); - gfx.setColor(painter.lineHighlightColor); - gfx.fillRect(x, y - - (painter.getFontMetrics().getHeight() - COMPOSING_UNDERBAR_HEIGHT), // - painter.getWidth(), painter.getFontMetrics().getHeight()); - } - - private void commitText(AttributedCharacterIterator text, int count) { - char c; - StringBuffer committing = new StringBuffer(count); - for (c = text.first(); c != AttributedCharacterIterator.DONE && count > 0; c = text - .next(), --count) { - committing.append(c); - } - int caret = textArea.getCaretPosition(); - String committing_text = committing.toString(); - try { - textArea.getDocument().insertString(caret, committing_text, null); - } catch (BadLocationException e) { - 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(); - composed.addAttribute(TextAttribute.FONT, font); - TextLayout layout = new TextLayout(composed.getIterator(), context); - return layout; - } - - public void caretPositionChanged(InputMethodEvent event) { - event.consume(); - } -} Index: app/src/processing/app/syntax/JEditTextArea.java =================================================================== --- app/src/processing/app/syntax/JEditTextArea.java (revision 6115) +++ app/src/processing/app/syntax/JEditTextArea.java (working copy) @@ -24,6 +24,8 @@ import java.util.Vector; import java.awt.im.InputMethodRequests; +import processing.app.syntax.im.InputMethodSupport; + /** * jEdit's text area component. It is more suited for editing program * source code than JEditorPane, because it drops the unnecessary features @@ -227,7 +229,7 @@ * Blinks the caret. */ public final void blinkCaret() { - if (caretBlinks && !((InputMethodSupport)getInputMethodRequests()).getIsComposing()) { + if (caretBlinks) { blink = !blink; painter.invalidateSelectedLines(); } else { Index: app/src/processing/app/syntax/TextAreaPainter.java =================================================================== --- app/src/processing/app/syntax/TextAreaPainter.java (revision 6115) +++ app/src/processing/app/syntax/TextAreaPainter.java (working copy) @@ -12,6 +12,7 @@ package processing.app.syntax; import processing.app.*; +import processing.app.syntax.im.CompositionTextPainter; import javax.swing.ToolTipManager; import javax.swing.text.*; @@ -33,6 +34,9 @@ /** Current setting for editor.antialias preference */ boolean antialias; + /** A specific painter composed by the InputMethod.*/ + protected CompositionTextPainter compositionTextPainter; + /** * Creates a new repaint manager. This should be not be called * directly. @@ -72,6 +76,16 @@ eolMarkerColor = defaults.eolMarkerColor; eolMarkers = defaults.eolMarkers; } + + /** + * Get CompositionTextPainter. if CompositionTextPainter is not created, create it. + */ + public CompositionTextPainter getCompositionTextpainter(){ + if(compositionTextPainter == null){ + compositionTextPainter = new CompositionTextPainter(textArea); + } + return compositionTextPainter; + } /** * Returns if this component can be traversed by pressing the @@ -602,7 +616,12 @@ y += fm.getHeight(); x = Utilities.drawTabbedText(currentLine,x,y,gfx,this,0); - + /* + * Draw characters via input method. + */ + if (compositionTextPainter != null && compositionTextPainter.hasComposedTextLayout()) { + compositionTextPainter.draw(gfx, lineHighlightColor); + } if (eolMarkers) { gfx.setColor(eolMarkerColor); gfx.drawString(".",x,y); @@ -625,7 +644,12 @@ x = SyntaxUtilities.paintSyntaxLine(currentLine, currentLineTokens, styles, this, gfx, x, y); - + /* + * Draw characters via input method. + */ + if (compositionTextPainter != null && compositionTextPainter.hasComposedTextLayout()) { + compositionTextPainter.draw(gfx, lineHighlightColor); + } if (eolMarkers) { gfx.setColor(eolMarkerColor); gfx.drawString(".",x,y); Index: app/src/processing/app/syntax/im/CompositionTextManager.java =================================================================== --- app/src/processing/app/syntax/im/CompositionTextManager.java (revision 0) +++ app/src/processing/app/syntax/im/CompositionTextManager.java (revision 0) @@ -0,0 +1,187 @@ +package processing.app.syntax.im; + +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.font.FontRenderContext; +import java.awt.font.TextAttribute; +import java.awt.font.TextLayout; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; + +import javax.swing.text.BadLocationException; + +import processing.app.syntax.JEditTextArea; +import processing.app.syntax.TextAreaPainter; + +/** + * This class Manage texts from input method + * by begin-process-end steps. + * + * First, if a user start inputing via input method, + * beginCompositionText is called from InputMethodSupport. + * Second, the user continues from input method, processCompositionText is called + * and reflect user inputs to text area. + * Finally the user try to commit text, endCompositionText is called. + * + * @author Takashi Maekawa (takachin@generative.info) + */ + +public class CompositionTextManager { + private JEditTextArea textArea; + private String prevComposeString; + private int prevCommittedCount; + private boolean isInputProcess; + private int initialCaretPosition; + public static final int COMPOSING_UNDERBAR_HEIGHT = 5; + + /** + * Create text manager class with a textarea. + * @param textArea texarea component for PDE. + */ + public CompositionTextManager(JEditTextArea textArea) { + this.textArea = textArea; + prevComposeString = ""; + isInputProcess = false; + prevCommittedCount = 0; + } + + /** + * Get this text manager is whether in input process or not. + */ + public boolean getIsInputProcess() { + return isInputProcess; + } + + /** + * Called when a user begins input from input method. + * This method initializes text manager. + * + * @param text Text from InputMethodEvent. + * @param commited_count Numbers of committed characters in text. + */ + public void beginCompositionText(AttributedCharacterIterator text, int committed_count) { + isInputProcess = true; + prevComposeString = ""; + initialCaretPosition = textArea.getCaretPosition(); + processCompositionText(text, committed_count); + } + + /** + * Called when a user processing input characters and + * select candidates from input method. + * + * @param text Text from InputMethodEvent. + * @param commited_count Numbers of committed characters in text. + */ + public void processCompositionText(AttributedCharacterIterator text, int committed_count) { + int layoutCaretPosition = initialCaretPosition + committed_count; + CompositionTextPainter compositionPainter = textArea.getPainter().getCompositionTextpainter(); + compositionPainter.setComposedTextLayout(getTextLayout(text, committed_count), layoutCaretPosition); + int textLength = text.getEndIndex() - text.getBeginIndex() - committed_count; + StringBuffer unCommitedStringBuf = new StringBuffer(textLength); + char c; + for (c = text.setIndex(committed_count); c != AttributedCharacterIterator.DONE + && textLength > 0; c = text.next(), --textLength) { + unCommitedStringBuf.append(c); + } + String unCommittedString = unCommitedStringBuf.toString(); + try { + if(canRemovePreviousInput(committed_count)){ + textArea.getDocument().remove(layoutCaretPosition, prevComposeString.length()); + } + textArea.getDocument().insertString(layoutCaretPosition, unCommittedString, null); + if(committed_count > 0){ + initialCaretPosition = initialCaretPosition + committed_count; + } + prevComposeString = unCommittedString; + prevCommittedCount = committed_count; + } catch (BadLocationException e) { + e.printStackTrace(); + } + } + + private boolean canRemovePreviousInput(int committed_count){ + return (prevCommittedCount == committed_count || prevCommittedCount > committed_count); + } + + /** + * Called when a user fixed text from input method or delete all + * composition text. This method resets CompositionTextPainter. + * + * @param text Text from InputMethodEvent. + * @param commited_count Numbers of committed characters in text. + */ + public void endCompositionText(AttributedCharacterIterator text, int committed_count) { + isInputProcess = false; + /* + * If there are no committed characters, remove it all from textarea. + * This case will happen if a user delete all composing characters by backspace or delete key. + * If it does, these previous characters are needed to be deleted. + */ + if(committed_count == 0){ + removeNotCommittedText(text); + } + CompositionTextPainter compositionPainter = textArea.getPainter().getCompositionTextpainter(); + compositionPainter.invalidateComposedTextLayout(initialCaretPosition + committed_count); + prevComposeString = ""; + isInputProcess = false; + } + + private void removeNotCommittedText(AttributedCharacterIterator text){ + if (prevComposeString.length() == 0) { + return; + } + try { + textArea.getDocument().remove(initialCaretPosition, prevComposeString.length()); + } catch (BadLocationException e) { + 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(); + composed.addAttribute(TextAttribute.FONT, font); + TextLayout layout = new TextLayout(composed.getIterator(), context); + return layout; + } + + private Point getCaretLocation() { + Point loc = new Point(); + TextAreaPainter painter = textArea.getPainter(); + FontMetrics fm = painter.getFontMetrics(); + int offsetY = fm.getHeight() - COMPOSING_UNDERBAR_HEIGHT; + int lineIndex = textArea.getCaretLine(); + loc.y = lineIndex * fm.getHeight() + offsetY; + int offsetX = textArea.getCaretPosition() + - textArea.getLineStartOffset(lineIndex); + loc.x = textArea.offsetToX(lineIndex, offsetX); + return loc; + } + + public Rectangle getTextLocation() { + Point caret = getCaretLocation(); + return getCaretRectangle(caret.x, caret.y); + } + + private Rectangle getCaretRectangle(int x, int y) { + TextAreaPainter painter = textArea.getPainter(); + Point origin = painter.getLocationOnScreen(); + int height = painter.getFontMetrics().getHeight(); + return new Rectangle(origin.x + x, origin.y + y, 0, height); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex) { + int length = endIndex - beginIndex; + String textAreaString = textArea.getText(beginIndex, length); + return new AttributedString(textAreaString).getIterator(); + } + + public int getInsertPositionOffset() { + return textArea.getCaretPosition() * -1; + } +} Index: app/src/processing/app/syntax/im/CompositionTextPainter.java =================================================================== --- app/src/processing/app/syntax/im/CompositionTextPainter.java (revision 0) +++ app/src/processing/app/syntax/im/CompositionTextPainter.java (revision 0) @@ -0,0 +1,125 @@ +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.font.TextLayout; + +import processing.app.syntax.JEditTextArea; +import processing.app.syntax.TextAreaPainter; + +/** + * Paint texts from input method. + * Text via input method are transmitted by AttributedCaharacterIterator. + * But current PDE's TextAreaPainter can't treat AttributedCaharacterIterator directly. + * So this class helps to treat it and paint text. + * + * For practical purposes, paint to textarea is done by TextLayout class. + * Because TextLayout class is easy to draw composing texts. + * (For example, draw underline composing texts, focus when select from candidates text.) + * + * @author Takashi Maekawa (takachin@generative.info) + */ +public class CompositionTextPainter { + private TextLayout composedTextLayout; + private int composedBeginCaretPosition = 0; + private JEditTextArea textArea; + + /** + * Constructor for painter. + * @param textarea textarea used by PDE. + */ + public CompositionTextPainter(JEditTextArea textArea) { + this.textArea = textArea; + composedTextLayout = null; + } + + /** + * Check the painter has TextLayout. + * If a user input via InputMethod, this result will return true. + * @param textarea textarea used by PDE. + */ + public boolean hasComposedTextLayout() { + return (composedTextLayout != null); + } + + /** + * Set TextLayout to the painter. + * TextLayout will be created and set by CompositionTextManager. + * + * @see CompositionTextManager + * @param textarea textarea used by PDE. + */ + public void setComposedTextLayout(TextLayout composedTextLayout, int composedStartCaretPosition) { + this.composedTextLayout = composedTextLayout; + this.composedBeginCaretPosition = composedStartCaretPosition; + } + + /** + * Invalidate this TextLayout to set null. + * If a user end input via InputMethod, this method will called from CompositionTextManager.endCompositionText + */ + public void invalidateComposedTextLayout(int composedEndCaretPosition) { + this.composedTextLayout = null; + this.composedBeginCaretPosition = composedEndCaretPosition; + //this.composedBeginCaretPosition = textArea.getCaretPosition(); + } + + /** + * Draw text via input method with composed text information. + * This method can draw texts with some underlines to illustrate converting characters. + * + * This method is workaround for TextAreaPainter. + * Because, TextAreaPainter can't treat AttributedCharacterIterator directly. + * AttributedCharacterIterator has very important information when composing text. + * It has a map where are converted characters and committed characters. + * Ideally, changing TextAreaPainter method can treat AttributedCharacterIterator is better. But it's very tough!! + * So I choose to write some code as a workaround. + * + * 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. + * + * @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); + } + + /** + * 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() { + Point loc = new Point(); + TextAreaPainter painter = textArea.getPainter(); + FontMetrics fm = painter.getFontMetrics(); + int offsetY = fm.getHeight() - CompositionTextManager.COMPOSING_UNDERBAR_HEIGHT; + int lineIndex = textArea.getCaretLine(); + loc.y = lineIndex * fm.getHeight() + offsetY; + int offsetX = composedBeginCaretPosition - textArea.getLineStartOffset(lineIndex); + loc.x = textArea.offsetToX(lineIndex, offsetX); + return loc; + } +} Index: app/src/processing/app/syntax/im/InputMethodSupport.java =================================================================== --- app/src/processing/app/syntax/im/InputMethodSupport.java (revision 0) +++ app/src/processing/app/syntax/im/InputMethodSupport.java (revision 0) @@ -0,0 +1,104 @@ +package processing.app.syntax.im; + +import java.awt.Rectangle; +import java.awt.event.InputMethodEvent; +import java.awt.event.InputMethodListener; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.text.AttributedCharacterIterator; + +import processing.app.syntax.JEditTextArea; + +/** + * 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. + * + * @author Takashi Maekawa (takachin@generative.info) + */ +public class InputMethodSupport implements InputMethodRequests, + InputMethodListener { + + private int committed_count = 0; + private CompositionTextManager textManager; + + public InputMethodSupport(JEditTextArea textArea) { + textManager = new CompositionTextManager(textArea); + textArea.enableInputMethods(true); + textArea.addInputMethodListener(this); + } + + public Rectangle getTextLocation(TextHitInfo offset) { + return textManager.getTextLocation(); + } + + public TextHitInfo getLocationOffset(int x, int y) { + return null; + } + + public int getInsertPositionOffset() { + return textManager.getInsertPositionOffset(); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, + int endIndex, AttributedCharacterIterator.Attribute[] attributes) { + return textManager.getCommittedText(beginIndex, endIndex); + } + + public int getCommittedTextLength() { + return committed_count; + } + + public AttributedCharacterIterator cancelLatestCommittedText( + AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText( + AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + /** + * 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(); + committed_count = event.getCommittedCharacterCount(); + if(isBeginInputProcess(text, textManager)){ + textManager.beginCompositionText(text, committed_count); + caretPositionChanged(event); + return; + } + if (isInputProcess(text)){ + textManager.processCompositionText(text, committed_count); + caretPositionChanged(event); + return; + } + textManager.endCompositionText(text, committed_count); + caretPositionChanged(event); + } + + private boolean isBeginInputProcess(AttributedCharacterIterator text, CompositionTextManager textManager){ + if(text == null) + return false; + return (isInputProcess(text) && !textManager.getIsInputProcess()); + } + + private boolean isInputProcess(AttributedCharacterIterator text){ + if(text == null) + return false; + return (text.getEndIndex() - (text.getBeginIndex() + committed_count) > 0); + } + + public void caretPositionChanged(InputMethodEvent event) { + event.consume(); + } +} Index: app/src/processing/app/syntax/im/CompositionTextManager.java =================================================================== --- app/src/processing/app/syntax/im/CompositionTextManager.java (revision 0) +++ app/src/processing/app/syntax/im/CompositionTextManager.java (revision 0) @@ -0,0 +1,187 @@ +package processing.app.syntax.im; + +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.font.FontRenderContext; +import java.awt.font.TextAttribute; +import java.awt.font.TextLayout; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; + +import javax.swing.text.BadLocationException; + +import processing.app.syntax.JEditTextArea; +import processing.app.syntax.TextAreaPainter; + +/** + * This class Manage texts from input method + * by begin-process-end steps. + * + * First, if a user start inputing via input method, + * beginCompositionText is called from InputMethodSupport. + * Second, the user continues from input method, processCompositionText is called + * and reflect user inputs to text area. + * Finally the user try to commit text, endCompositionText is called. + * + * @author Takashi Maekawa (takachin@generative.info) + */ + +public class CompositionTextManager { + private JEditTextArea textArea; + private String prevComposeString; + private int prevCommittedCount; + private boolean isInputProcess; + private int initialCaretPosition; + public static final int COMPOSING_UNDERBAR_HEIGHT = 5; + + /** + * Create text manager class with a textarea. + * @param textArea texarea component for PDE. + */ + public CompositionTextManager(JEditTextArea textArea) { + this.textArea = textArea; + prevComposeString = ""; + isInputProcess = false; + prevCommittedCount = 0; + } + + /** + * Get this text manager is whether in input process or not. + */ + public boolean getIsInputProcess() { + return isInputProcess; + } + + /** + * Called when a user begins input from input method. + * This method initializes text manager. + * + * @param text Text from InputMethodEvent. + * @param commited_count Numbers of committed characters in text. + */ + public void beginCompositionText(AttributedCharacterIterator text, int committed_count) { + isInputProcess = true; + prevComposeString = ""; + initialCaretPosition = textArea.getCaretPosition(); + processCompositionText(text, committed_count); + } + + /** + * Called when a user processing input characters and + * select candidates from input method. + * + * @param text Text from InputMethodEvent. + * @param commited_count Numbers of committed characters in text. + */ + public void processCompositionText(AttributedCharacterIterator text, int committed_count) { + int layoutCaretPosition = initialCaretPosition + committed_count; + CompositionTextPainter compositionPainter = textArea.getPainter().getCompositionTextpainter(); + compositionPainter.setComposedTextLayout(getTextLayout(text, committed_count), layoutCaretPosition); + int textLength = text.getEndIndex() - text.getBeginIndex() - committed_count; + StringBuffer unCommitedStringBuf = new StringBuffer(textLength); + char c; + for (c = text.setIndex(committed_count); c != AttributedCharacterIterator.DONE + && textLength > 0; c = text.next(), --textLength) { + unCommitedStringBuf.append(c); + } + String unCommittedString = unCommitedStringBuf.toString(); + try { + if(canRemovePreviousInput(committed_count)){ + textArea.getDocument().remove(layoutCaretPosition, prevComposeString.length()); + } + textArea.getDocument().insertString(layoutCaretPosition, unCommittedString, null); + if(committed_count > 0){ + initialCaretPosition = initialCaretPosition + committed_count; + } + prevComposeString = unCommittedString; + prevCommittedCount = committed_count; + } catch (BadLocationException e) { + e.printStackTrace(); + } + } + + private boolean canRemovePreviousInput(int committed_count){ + return (prevCommittedCount == committed_count || prevCommittedCount > committed_count); + } + + /** + * Called when a user fixed text from input method or delete all + * composition text. This method resets CompositionTextPainter. + * + * @param text Text from InputMethodEvent. + * @param commited_count Numbers of committed characters in text. + */ + public void endCompositionText(AttributedCharacterIterator text, int committed_count) { + isInputProcess = false; + /* + * If there are no committed characters, remove it all from textarea. + * This case will happen if a user delete all composing characters by backspace or delete key. + * If it does, these previous characters are needed to be deleted. + */ + if(committed_count == 0){ + removeNotCommittedText(text); + } + CompositionTextPainter compositionPainter = textArea.getPainter().getCompositionTextpainter(); + compositionPainter.invalidateComposedTextLayout(initialCaretPosition + committed_count); + prevComposeString = ""; + isInputProcess = false; + } + + private void removeNotCommittedText(AttributedCharacterIterator text){ + if (prevComposeString.length() == 0) { + return; + } + try { + textArea.getDocument().remove(initialCaretPosition, prevComposeString.length()); + } catch (BadLocationException e) { + 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(); + composed.addAttribute(TextAttribute.FONT, font); + TextLayout layout = new TextLayout(composed.getIterator(), context); + return layout; + } + + private Point getCaretLocation() { + Point loc = new Point(); + TextAreaPainter painter = textArea.getPainter(); + FontMetrics fm = painter.getFontMetrics(); + int offsetY = fm.getHeight() - COMPOSING_UNDERBAR_HEIGHT; + int lineIndex = textArea.getCaretLine(); + loc.y = lineIndex * fm.getHeight() + offsetY; + int offsetX = textArea.getCaretPosition() + - textArea.getLineStartOffset(lineIndex); + loc.x = textArea.offsetToX(lineIndex, offsetX); + return loc; + } + + public Rectangle getTextLocation() { + Point caret = getCaretLocation(); + return getCaretRectangle(caret.x, caret.y); + } + + private Rectangle getCaretRectangle(int x, int y) { + TextAreaPainter painter = textArea.getPainter(); + Point origin = painter.getLocationOnScreen(); + int height = painter.getFontMetrics().getHeight(); + return new Rectangle(origin.x + x, origin.y + y, 0, height); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex) { + int length = endIndex - beginIndex; + String textAreaString = textArea.getText(beginIndex, length); + return new AttributedString(textAreaString).getIterator(); + } + + public int getInsertPositionOffset() { + return textArea.getCaretPosition() * -1; + } +} Index: app/src/processing/app/syntax/im/CompositionTextPainter.java =================================================================== --- app/src/processing/app/syntax/im/CompositionTextPainter.java (revision 0) +++ app/src/processing/app/syntax/im/CompositionTextPainter.java (revision 0) @@ -0,0 +1,125 @@ +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.font.TextLayout; + +import processing.app.syntax.JEditTextArea; +import processing.app.syntax.TextAreaPainter; + +/** + * Paint texts from input method. + * Text via input method are transmitted by AttributedCaharacterIterator. + * But current PDE's TextAreaPainter can't treat AttributedCaharacterIterator directly. + * So this class helps to treat it and paint text. + * + * For practical purposes, paint to textarea is done by TextLayout class. + * Because TextLayout class is easy to draw composing texts. + * (For example, draw underline composing texts, focus when select from candidates text.) + * + * @author Takashi Maekawa (takachin@generative.info) + */ +public class CompositionTextPainter { + private TextLayout composedTextLayout; + private int composedBeginCaretPosition = 0; + private JEditTextArea textArea; + + /** + * Constructor for painter. + * @param textarea textarea used by PDE. + */ + public CompositionTextPainter(JEditTextArea textArea) { + this.textArea = textArea; + composedTextLayout = null; + } + + /** + * Check the painter has TextLayout. + * If a user input via InputMethod, this result will return true. + * @param textarea textarea used by PDE. + */ + public boolean hasComposedTextLayout() { + return (composedTextLayout != null); + } + + /** + * Set TextLayout to the painter. + * TextLayout will be created and set by CompositionTextManager. + * + * @see CompositionTextManager + * @param textarea textarea used by PDE. + */ + public void setComposedTextLayout(TextLayout composedTextLayout, int composedStartCaretPosition) { + this.composedTextLayout = composedTextLayout; + this.composedBeginCaretPosition = composedStartCaretPosition; + } + + /** + * Invalidate this TextLayout to set null. + * If a user end input via InputMethod, this method will called from CompositionTextManager.endCompositionText + */ + public void invalidateComposedTextLayout(int composedEndCaretPosition) { + this.composedTextLayout = null; + this.composedBeginCaretPosition = composedEndCaretPosition; + //this.composedBeginCaretPosition = textArea.getCaretPosition(); + } + + /** + * Draw text via input method with composed text information. + * This method can draw texts with some underlines to illustrate converting characters. + * + * This method is workaround for TextAreaPainter. + * Because, TextAreaPainter can't treat AttributedCharacterIterator directly. + * AttributedCharacterIterator has very important information when composing text. + * It has a map where are converted characters and committed characters. + * Ideally, changing TextAreaPainter method can treat AttributedCharacterIterator is better. But it's very tough!! + * So I choose to write some code as a workaround. + * + * 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. + * + * @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); + } + + /** + * 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() { + Point loc = new Point(); + TextAreaPainter painter = textArea.getPainter(); + FontMetrics fm = painter.getFontMetrics(); + int offsetY = fm.getHeight() - CompositionTextManager.COMPOSING_UNDERBAR_HEIGHT; + int lineIndex = textArea.getCaretLine(); + loc.y = lineIndex * fm.getHeight() + offsetY; + int offsetX = composedBeginCaretPosition - textArea.getLineStartOffset(lineIndex); + loc.x = textArea.offsetToX(lineIndex, offsetX); + return loc; + } +} Index: app/src/processing/app/syntax/im/InputMethodSupport.java =================================================================== --- app/src/processing/app/syntax/im/InputMethodSupport.java (revision 0) +++ app/src/processing/app/syntax/im/InputMethodSupport.java (revision 0) @@ -0,0 +1,104 @@ +package processing.app.syntax.im; + +import java.awt.Rectangle; +import java.awt.event.InputMethodEvent; +import java.awt.event.InputMethodListener; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.text.AttributedCharacterIterator; + +import processing.app.syntax.JEditTextArea; + +/** + * 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. + * + * @author Takashi Maekawa (takachin@generative.info) + */ +public class InputMethodSupport implements InputMethodRequests, + InputMethodListener { + + private int committed_count = 0; + private CompositionTextManager textManager; + + public InputMethodSupport(JEditTextArea textArea) { + textManager = new CompositionTextManager(textArea); + textArea.enableInputMethods(true); + textArea.addInputMethodListener(this); + } + + public Rectangle getTextLocation(TextHitInfo offset) { + return textManager.getTextLocation(); + } + + public TextHitInfo getLocationOffset(int x, int y) { + return null; + } + + public int getInsertPositionOffset() { + return textManager.getInsertPositionOffset(); + } + + public AttributedCharacterIterator getCommittedText(int beginIndex, + int endIndex, AttributedCharacterIterator.Attribute[] attributes) { + return textManager.getCommittedText(beginIndex, endIndex); + } + + public int getCommittedTextLength() { + return committed_count; + } + + public AttributedCharacterIterator cancelLatestCommittedText( + AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + public AttributedCharacterIterator getSelectedText( + AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + /** + * 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(); + committed_count = event.getCommittedCharacterCount(); + if(isBeginInputProcess(text, textManager)){ + textManager.beginCompositionText(text, committed_count); + caretPositionChanged(event); + return; + } + if (isInputProcess(text)){ + textManager.processCompositionText(text, committed_count); + caretPositionChanged(event); + return; + } + textManager.endCompositionText(text, committed_count); + caretPositionChanged(event); + } + + private boolean isBeginInputProcess(AttributedCharacterIterator text, CompositionTextManager textManager){ + if(text == null) + return false; + return (isInputProcess(text) && !textManager.getIsInputProcess()); + } + + private boolean isInputProcess(AttributedCharacterIterator text){ + if(text == null) + return false; + return (text.getEndIndex() - (text.getBeginIndex() + committed_count) > 0); + } + + public void caretPositionChanged(InputMethodEvent event) { + event.consume(); + } +}