J. Duke | 319a3b9 | 2007-12-01 00:00:00 +0000 | [diff] [blame^] | 1 | /* |
| 2 | * Copyright 1997-2006 Sun Microsystems, Inc. All Rights Reserved. |
| 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| 4 | * |
| 5 | * This code is free software; you can redistribute it and/or modify it |
| 6 | * under the terms of the GNU General Public License version 2 only, as |
| 7 | * published by the Free Software Foundation. Sun designates this |
| 8 | * particular file as subject to the "Classpath" exception as provided |
| 9 | * by Sun in the LICENSE file that accompanied this code. |
| 10 | * |
| 11 | * This code is distributed in the hope that it will be useful, but WITHOUT |
| 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| 14 | * version 2 for more details (a copy is included in the LICENSE file that |
| 15 | * accompanied this code). |
| 16 | * |
| 17 | * You should have received a copy of the GNU General Public License version |
| 18 | * 2 along with this work; if not, write to the Free Software Foundation, |
| 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| 20 | * |
| 21 | * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, |
| 22 | * CA 95054 USA or visit www.sun.com if you need additional information or |
| 23 | * have any questions. |
| 24 | */ |
| 25 | |
| 26 | package sun.awt.im; |
| 27 | |
| 28 | import java.awt.AWTEvent; |
| 29 | import java.awt.Color; |
| 30 | import java.awt.Dimension; |
| 31 | import java.awt.FontMetrics; |
| 32 | import java.awt.Graphics; |
| 33 | import java.awt.Graphics2D; |
| 34 | import java.awt.Point; |
| 35 | import java.awt.Rectangle; |
| 36 | import java.awt.Toolkit; |
| 37 | import java.awt.event.InputMethodEvent; |
| 38 | import java.awt.event.InputMethodListener; |
| 39 | import java.awt.event.WindowEvent; |
| 40 | import java.awt.event.WindowAdapter; |
| 41 | import java.awt.font.FontRenderContext; |
| 42 | import java.awt.font.TextHitInfo; |
| 43 | import java.awt.font.TextLayout; |
| 44 | import java.awt.geom.Rectangle2D; |
| 45 | import java.awt.im.InputMethodRequests; |
| 46 | import java.text.AttributedCharacterIterator; |
| 47 | import javax.swing.JFrame; |
| 48 | import javax.swing.JPanel; |
| 49 | import javax.swing.border.LineBorder; |
| 50 | |
| 51 | /** |
| 52 | * A composition area is used to display text that's being composed |
| 53 | * using an input method in its own user interface environment, |
| 54 | * typically in a root window. |
| 55 | * |
| 56 | * @author JavaSoft International |
| 57 | */ |
| 58 | |
| 59 | public class CompositionArea extends JPanel implements InputMethodListener { |
| 60 | |
| 61 | private CompositionAreaHandler handler; |
| 62 | |
| 63 | private TextLayout composedTextLayout; |
| 64 | private TextHitInfo caret = null; |
| 65 | private JFrame compositionWindow; |
| 66 | private final static int TEXT_ORIGIN_X = 5; |
| 67 | private final static int TEXT_ORIGIN_Y = 15; |
| 68 | private final static int PASSIVE_WIDTH = 480; |
| 69 | private final static int WIDTH_MARGIN=10; |
| 70 | private final static int HEIGHT_MARGIN=3; |
| 71 | |
| 72 | CompositionArea() { |
| 73 | // create composition window with localized title |
| 74 | String windowTitle = Toolkit.getProperty("AWT.CompositionWindowTitle", "Input Window"); |
| 75 | compositionWindow = |
| 76 | (JFrame)InputMethodContext.createInputMethodWindow(windowTitle, null, true); |
| 77 | |
| 78 | setOpaque(true); |
| 79 | setBorder(LineBorder.createGrayLineBorder()); |
| 80 | setForeground(Color.black); |
| 81 | setBackground(Color.white); |
| 82 | |
| 83 | // if we get the focus, we still want to let the client's |
| 84 | // input context handle the event |
| 85 | enableInputMethods(true); |
| 86 | enableEvents(AWTEvent.KEY_EVENT_MASK); |
| 87 | |
| 88 | compositionWindow.getContentPane().add(this); |
| 89 | compositionWindow.addWindowListener(new FrameWindowAdapter()); |
| 90 | addInputMethodListener(this); |
| 91 | compositionWindow.enableInputMethods(false); |
| 92 | compositionWindow.pack(); |
| 93 | Dimension windowSize = compositionWindow.getSize(); |
| 94 | Dimension screenSize = (getToolkit()).getScreenSize(); |
| 95 | compositionWindow.setLocation(screenSize.width - windowSize.width-20, |
| 96 | screenSize.height - windowSize.height-100); |
| 97 | compositionWindow.setVisible(false); |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * Sets the composition area handler that currently owns this |
| 102 | * composition area, and its input context. |
| 103 | */ |
| 104 | synchronized void setHandlerInfo(CompositionAreaHandler handler, InputContext inputContext) { |
| 105 | this.handler = handler; |
| 106 | ((InputMethodWindow) compositionWindow).setInputContext(inputContext); |
| 107 | } |
| 108 | |
| 109 | /** |
| 110 | * @see java.awt.Component#getInputMethodRequests |
| 111 | */ |
| 112 | public InputMethodRequests getInputMethodRequests() { |
| 113 | return handler; |
| 114 | } |
| 115 | |
| 116 | // returns a 0-width rectangle |
| 117 | private Rectangle getCaretRectangle(TextHitInfo caret) { |
| 118 | int caretLocation = 0; |
| 119 | TextLayout layout = composedTextLayout; |
| 120 | if (layout != null) { |
| 121 | caretLocation = Math.round(layout.getCaretInfo(caret)[0]); |
| 122 | } |
| 123 | Graphics g = getGraphics(); |
| 124 | FontMetrics metrics = null; |
| 125 | try { |
| 126 | metrics = g.getFontMetrics(); |
| 127 | } finally { |
| 128 | g.dispose(); |
| 129 | } |
| 130 | return new Rectangle(TEXT_ORIGIN_X + caretLocation, |
| 131 | TEXT_ORIGIN_Y - metrics.getAscent(), |
| 132 | 0, metrics.getAscent() + metrics.getDescent()); |
| 133 | } |
| 134 | |
| 135 | public void paint(Graphics g) { |
| 136 | super.paint(g); |
| 137 | g.setColor(getForeground()); |
| 138 | TextLayout layout = composedTextLayout; |
| 139 | if (layout != null) { |
| 140 | layout.draw((Graphics2D) g, TEXT_ORIGIN_X, TEXT_ORIGIN_Y); |
| 141 | } |
| 142 | if (caret != null) { |
| 143 | Rectangle rectangle = getCaretRectangle(caret); |
| 144 | g.setXORMode(getBackground()); |
| 145 | g.fillRect(rectangle.x, rectangle.y, 1, rectangle.height); |
| 146 | g.setPaintMode(); |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | // shows/hides the composition window |
| 151 | void setCompositionAreaVisible(boolean visible) { |
| 152 | compositionWindow.setVisible(visible); |
| 153 | } |
| 154 | |
| 155 | // returns true if composition area is visible |
| 156 | boolean isCompositionAreaVisible() { |
| 157 | return compositionWindow.isVisible(); |
| 158 | } |
| 159 | |
| 160 | // workaround for the Solaris focus lost problem |
| 161 | class FrameWindowAdapter extends WindowAdapter { |
| 162 | public void windowActivated(WindowEvent e) { |
| 163 | requestFocus(); |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | // InputMethodListener methods - just forward to the current handler |
| 168 | public void inputMethodTextChanged(InputMethodEvent event) { |
| 169 | handler.inputMethodTextChanged(event); |
| 170 | } |
| 171 | |
| 172 | public void caretPositionChanged(InputMethodEvent event) { |
| 173 | handler.caretPositionChanged(event); |
| 174 | } |
| 175 | |
| 176 | /** |
| 177 | * Sets the text and caret to be displayed in this composition area. |
| 178 | * Shows the window if it contains text, hides it if not. |
| 179 | */ |
| 180 | void setText(AttributedCharacterIterator composedText, TextHitInfo caret) { |
| 181 | composedTextLayout = null; |
| 182 | if (composedText == null) { |
| 183 | // there's no composed text to display, so hide the window |
| 184 | compositionWindow.setVisible(false); |
| 185 | this.caret = null; |
| 186 | } else { |
| 187 | /* since we have composed text, make sure the window is shown. |
| 188 | This is necessary to get a valid graphics object. See 6181385. |
| 189 | */ |
| 190 | if (!compositionWindow.isVisible()) { |
| 191 | compositionWindow.setVisible(true); |
| 192 | } |
| 193 | |
| 194 | Graphics g = getGraphics(); |
| 195 | |
| 196 | if (g == null) { |
| 197 | return; |
| 198 | } |
| 199 | |
| 200 | try { |
| 201 | updateWindowLocation(); |
| 202 | |
| 203 | FontRenderContext context = ((Graphics2D)g).getFontRenderContext(); |
| 204 | composedTextLayout = new TextLayout(composedText, context); |
| 205 | Rectangle2D bounds = composedTextLayout.getBounds(); |
| 206 | |
| 207 | this.caret = caret; |
| 208 | |
| 209 | // Resize the composition area to just fit the text. |
| 210 | FontMetrics metrics = g.getFontMetrics(); |
| 211 | Rectangle2D maxCharBoundsRec = metrics.getMaxCharBounds(g); |
| 212 | int newHeight = (int)maxCharBoundsRec.getHeight() + HEIGHT_MARGIN; |
| 213 | int newFrameHeight = newHeight +compositionWindow.getInsets().top |
| 214 | +compositionWindow.getInsets().bottom; |
| 215 | // If it's a passive client, set the width always to PASSIVE_WIDTH (480px) |
| 216 | InputMethodRequests req = handler.getClientInputMethodRequests(); |
| 217 | int newWidth = (req==null) ? PASSIVE_WIDTH : (int)bounds.getWidth() + WIDTH_MARGIN; |
| 218 | int newFrameWidth = newWidth + compositionWindow.getInsets().left |
| 219 | + compositionWindow.getInsets().right; |
| 220 | setPreferredSize(new Dimension(newWidth, newHeight)); |
| 221 | compositionWindow.setSize(new Dimension(newFrameWidth, newFrameHeight)); |
| 222 | |
| 223 | // show the composed text |
| 224 | paint(g); |
| 225 | } |
| 226 | finally { |
| 227 | g.dispose(); |
| 228 | } |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | /** |
| 233 | * Sets the caret to be displayed in this composition area. |
| 234 | * The text is not changed. |
| 235 | */ |
| 236 | void setCaret(TextHitInfo caret) { |
| 237 | this.caret = caret; |
| 238 | if (compositionWindow.isVisible()) { |
| 239 | Graphics g = getGraphics(); |
| 240 | try { |
| 241 | paint(g); |
| 242 | } finally { |
| 243 | g.dispose(); |
| 244 | } |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | /** |
| 249 | * Positions the composition window near (usually below) the |
| 250 | * insertion point in the client component if the client |
| 251 | * component is an active client (below-the-spot input). |
| 252 | */ |
| 253 | void updateWindowLocation() { |
| 254 | InputMethodRequests req = handler.getClientInputMethodRequests(); |
| 255 | if (req == null) { |
| 256 | // not an active client |
| 257 | return; |
| 258 | } |
| 259 | |
| 260 | Point windowLocation = new Point(); |
| 261 | |
| 262 | Rectangle caretRect = req.getTextLocation(null); |
| 263 | Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); |
| 264 | Dimension windowSize = compositionWindow.getSize(); |
| 265 | final int SPACING = 2; |
| 266 | |
| 267 | if (caretRect.x + windowSize.width > screenSize.width) { |
| 268 | windowLocation.x = screenSize.width - windowSize.width; |
| 269 | } else { |
| 270 | windowLocation.x = caretRect.x; |
| 271 | } |
| 272 | |
| 273 | if (caretRect.y + caretRect.height + SPACING + windowSize.height > screenSize.height) { |
| 274 | windowLocation.y = caretRect.y - SPACING - windowSize.height; |
| 275 | } else { |
| 276 | windowLocation.y = caretRect.y + caretRect.height + SPACING; |
| 277 | } |
| 278 | |
| 279 | compositionWindow.setLocation(windowLocation); |
| 280 | } |
| 281 | |
| 282 | // support for InputMethodRequests methods |
| 283 | Rectangle getTextLocation(TextHitInfo offset) { |
| 284 | Rectangle rectangle = getCaretRectangle(offset); |
| 285 | Point location = getLocationOnScreen(); |
| 286 | rectangle.translate(location.x, location.y); |
| 287 | return rectangle; |
| 288 | } |
| 289 | |
| 290 | TextHitInfo getLocationOffset(int x, int y) { |
| 291 | TextLayout layout = composedTextLayout; |
| 292 | if (layout == null) { |
| 293 | return null; |
| 294 | } else { |
| 295 | Point location = getLocationOnScreen(); |
| 296 | x -= location.x + TEXT_ORIGIN_X; |
| 297 | y -= location.y + TEXT_ORIGIN_Y; |
| 298 | if (layout.getBounds().contains(x, y)) { |
| 299 | return layout.hitTestChar(x, y); |
| 300 | } else { |
| 301 | return null; |
| 302 | } |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | // Disables or enables decorations of the composition window |
| 307 | void setCompositionAreaUndecorated(boolean setUndecorated){ |
| 308 | if (compositionWindow.isDisplayable()){ |
| 309 | compositionWindow.removeNotify(); |
| 310 | } |
| 311 | compositionWindow.setUndecorated(setUndecorated); |
| 312 | compositionWindow.pack(); |
| 313 | } |
| 314 | |
| 315 | } |