/* * InputMethodSupport.java - Input method support for JEditTextArea * * :tabSize=4:indentSize=4:noTabs=false: * :folding=explicit:collapseFolds=1: * * Copyright (C) 2006 Kazutoshi Satoda * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.gjt.sp.jedit.textarea; // {{{ Imports import java.text.AttributedString; import java.text.AttributedCharacterIterator; import java.awt.Point; import java.awt.Rectangle; import java.awt.Graphics2D; import java.awt.FontMetrics; import java.awt.im.InputMethodRequests; import java.awt.event.InputMethodListener; import java.awt.event.InputMethodEvent; import java.awt.font.TextLayout; import java.awt.font.TextAttribute; import java.awt.font.TextHitInfo; // }}} /** * Input method support for JEditTextArea * * @author Kazutoshi Satoda * @since jEdit 4.3pre7 */ class InputMethodSupport extends TextAreaExtension implements InputMethodRequests, InputMethodListener { // The owner. private TextArea owner; // The composed text layout which was built from last InputMethodEvent. private TextLayout composedTextLayout = null; // The X offset to the caret in the composed text. private int composedCaretX = 0; // Last committed information to support cancelLatestCommittedText() private int lastCommittedAt = 0; private String lastCommittedText = null; public InputMethodSupport(TextArea owner) { this.owner = owner; owner.addInputMethodListener(this); owner.getPainter().addExtension(TextAreaPainter.HIGHEST_LAYER, this); } // {{{ Private utilities // Compute return value of getTextLocation() from (x, y). private Rectangle getCaretRectangle(int x, int y) { TextAreaPainter painter = owner.getPainter(); Point origin = painter.getLocationOnScreen(); int height = painter.getLineHeight(); return new Rectangle(origin.x + x, origin.y + y, 0, height); } // }}} // {{{ extends TextAreaExtension public void paintValidLine(Graphics2D gfx, int screenLine, int physicalLine, int start, int end, int y) { if(composedTextLayout != null) { int caret = owner.getCaretPosition(); if(start <= caret && caret < end) { TextAreaPainter painter = owner.getPainter(); // The hight and baseline are taken from // painter's FontMetrics instead of TextLayout // so that the composed text is rendered at // the same position with text in the TextArea. FontMetrics fm = painter.getFontMetrics(); int x = owner.offsetToXY(caret).x; int width = Math.round(composedTextLayout.getAdvance()); int height = painter.getLineHeight(); int offset_to_baseline = height - (fm.getLeading()+1) - fm.getDescent(); int caret_x = x + composedCaretX; gfx.setColor(painter.getBackground()); gfx.fillRect(x, y, width, height); gfx.setColor(painter.getForeground()); composedTextLayout.draw(gfx, x, y + offset_to_baseline); gfx.setColor(painter.getCaretColor()); gfx.drawLine(caret_x, y, caret_x, y + height - 1); } } } // }}} // {{{ implements InputMethodRequests public Rectangle getTextLocation(TextHitInfo offset) { if(composedTextLayout != null) { // return location of composed text. Point caret = owner.offsetToXY(owner.getCaretPosition()); return getCaretRectangle(caret.x + composedCaretX, caret.y); } else { // return location of selected text. Selection selection_on_caret = owner.getSelectionAtOffset(owner.getCaretPosition()); if(selection_on_caret != null) { Point selection_start = owner.offsetToXY(selection_on_caret.getStart()); return getCaretRectangle(selection_start.x, selection_start.y); } } return null; } public TextHitInfo getLocationOffset(int x, int y) { if(composedTextLayout != null) { Point origin = owner.getPainter().getLocationOnScreen(); Point caret = owner.offsetToXY(owner.getCaretPosition()); float local_x = x - origin.x - caret.x; float local_y = y - origin.y - caret.y - (composedTextLayout.getLeading()+1) - composedTextLayout.getAscent(); return composedTextLayout.hitTestChar(local_x, local_y); } return null; } public int getInsertPositionOffset() { return owner.getCaretPosition(); } public AttributedCharacterIterator getCommittedText(int beginIndex , int endIndex , AttributedCharacterIterator.Attribute[] attributes) { return (new AttributedString(owner.getText(beginIndex, endIndex - beginIndex))).getIterator(); } public int getCommittedTextLength() { return owner.getBufferLength(); } public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { if(lastCommittedText != null) { int offset = lastCommittedAt; int length = lastCommittedText.length(); String sample = owner.getText(offset, length); if(sample != null && sample.equals(lastCommittedText)) { AttributedCharacterIterator canceled = (new AttributedString(sample)).getIterator(); owner.getBuffer().remove(offset, length); owner.setCaretPosition(offset); lastCommittedText = null; return canceled; } // Cleare last committed information to prevent // accidental match. lastCommittedText = null; } return null; } public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { Selection selection_on_caret = owner.getSelectionAtOffset(owner.getCaretPosition()); if(selection_on_caret != null) { return (new AttributedString(owner.getSelectedText(selection_on_caret))).getIterator(); } return null; } // }}} // {{{ implements InputMethodListener public void inputMethodTextChanged(InputMethodEvent event) { composedTextLayout = null; AttributedCharacterIterator text = event.getText(); if(text != null) { int committed_count = event.getCommittedCharacterCount(); if(committed_count > 0) { lastCommittedText = null; lastCommittedAt = owner.getCaretPosition(); StringBuilder committed = new StringBuilder(committed_count); char c; int count; for(c = text.first(), count = committed_count ; c != AttributedCharacterIterator.DONE && count > 0 ; c = text.next(), --count) { owner.userInput(c); committed.append(c); } lastCommittedText = committed.toString(); } int end_index = text.getEndIndex(); if(committed_count < end_index) { AttributedString composed = new AttributedString(text, committed_count, end_index); TextAreaPainter painter = owner.getPainter(); composed.addAttribute(TextAttribute.FONT, painter.getFont()); composedTextLayout = new TextLayout(composed.getIterator() , painter.getFontRenderContext()); } } // Also updates caret. caretPositionChanged(event); } public void caretPositionChanged(InputMethodEvent event) { composedCaretX = 0; if(composedTextLayout != null) { TextHitInfo caret = event.getCaret(); if(caret != null) { composedCaretX = Math.round(composedTextLayout.getCaretInfo(caret)[0]); } // Adjust visiblity. int insertion_x = owner.offsetToXY(owner.getCaretPosition()).x; TextHitInfo visible = event.getVisiblePosition(); int composed_visible_x = (visible != null) ? Math.round(composedTextLayout.getCaretInfo(visible)[0]) : composedCaretX; int visible_x = insertion_x + composed_visible_x; int painter_width = owner.getPainter().getWidth(); int adjustment = 0; if(visible_x < 0) { adjustment = visible_x; } if(visible_x >= painter_width) { adjustment = visible_x - (painter_width - 1); } if(adjustment != 0) { owner.setHorizontalOffset(owner.getHorizontalOffset() - adjustment); } } else { /* Cancel horizontal adjustment for composed text. FIXME: The horizontal offset may be beyond the max value of owner's horizontal scroll bar. */ owner.scrollToCaret(false); } /* Invalidate one more line below the caret because the underline for composed text goes beyond the caret line in some font settings. */ int caret_line = owner.getCaretLine(); owner.invalidateLineRange(caret_line, caret_line + 1); event.consume(); } // }}} }