1 /* 2 * Copyright (c) 1997, 2021, Oracle and/or its affiliates. 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. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * 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.WindowAdapter; 40 import java.awt.event.WindowEvent; 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.io.Serial; 47 import java.text.AttributedCharacterIterator; 48 49 import javax.swing.JFrame; 50 import javax.swing.JPanel; 51 import javax.swing.border.LineBorder; 52 53 /** 54 * A composition area is used to display text that's being composed 55 * using an input method in its own user interface environment, 56 * typically in a root window. 57 * 58 * @author JavaSoft International 59 */ 60 61 // This class is final due to the 6607310 fix. Refer to the CR for details. 62 public final class CompositionArea extends JPanel implements InputMethodListener { 63 64 private CompositionAreaHandler handler; 65 66 private TextLayout composedTextLayout; 67 private TextHitInfo caret = null; 68 private JFrame compositionWindow; 69 private static final int TEXT_ORIGIN_X = 5; 70 private static final int TEXT_ORIGIN_Y = 15; 71 private static final int PASSIVE_WIDTH = 480; 72 private static final int WIDTH_MARGIN=10; 73 private static final int HEIGHT_MARGIN=3; 74 CompositionArea()75 CompositionArea() { 76 // create composition window with localized title 77 String windowTitle = Toolkit.getProperty("AWT.CompositionWindowTitle", "Input Window"); 78 compositionWindow = 79 (JFrame)InputMethodContext.createInputMethodWindow(windowTitle, null, true); 80 81 setOpaque(true); 82 setBorder(LineBorder.createGrayLineBorder()); 83 setForeground(Color.black); 84 setBackground(Color.white); 85 86 // if we get the focus, we still want to let the client's 87 // input context handle the event 88 enableInputMethods(true); 89 enableEvents(AWTEvent.KEY_EVENT_MASK); 90 91 compositionWindow.getContentPane().add(this); 92 compositionWindow.addWindowListener(new FrameWindowAdapter()); 93 addInputMethodListener(this); 94 compositionWindow.enableInputMethods(false); 95 compositionWindow.pack(); 96 Dimension windowSize = compositionWindow.getSize(); 97 Dimension screenSize = (getToolkit()).getScreenSize(); 98 compositionWindow.setLocation(screenSize.width - windowSize.width-20, 99 screenSize.height - windowSize.height-100); 100 compositionWindow.setVisible(false); 101 } 102 103 /** 104 * Sets the composition area handler that currently owns this 105 * composition area, and its input context. 106 */ setHandlerInfo(CompositionAreaHandler handler, InputContext inputContext)107 synchronized void setHandlerInfo(CompositionAreaHandler handler, InputContext inputContext) { 108 this.handler = handler; 109 ((InputMethodWindow) compositionWindow).setInputContext(inputContext); 110 } 111 112 /** 113 * @see java.awt.Component#getInputMethodRequests 114 */ getInputMethodRequests()115 public InputMethodRequests getInputMethodRequests() { 116 return handler; 117 } 118 119 // returns a 0-width rectangle getCaretRectangle(TextHitInfo caret)120 private Rectangle getCaretRectangle(TextHitInfo caret) { 121 int caretLocation = 0; 122 TextLayout layout = composedTextLayout; 123 if (layout != null) { 124 caretLocation = Math.round(layout.getCaretInfo(caret)[0]); 125 } 126 Graphics g = getGraphics(); 127 FontMetrics metrics = null; 128 try { 129 metrics = g.getFontMetrics(); 130 } finally { 131 g.dispose(); 132 } 133 return new Rectangle(TEXT_ORIGIN_X + caretLocation, 134 TEXT_ORIGIN_Y - metrics.getAscent(), 135 0, metrics.getAscent() + metrics.getDescent()); 136 } 137 paint(Graphics g)138 public void paint(Graphics g) { 139 super.paint(g); 140 g.setColor(getForeground()); 141 TextLayout layout = composedTextLayout; 142 if (layout != null) { 143 layout.draw((Graphics2D) g, TEXT_ORIGIN_X, TEXT_ORIGIN_Y); 144 } 145 if (caret != null) { 146 Rectangle rectangle = getCaretRectangle(caret); 147 g.setXORMode(getBackground()); 148 g.fillRect(rectangle.x, rectangle.y, 1, rectangle.height); 149 g.setPaintMode(); 150 } 151 } 152 153 // shows/hides the composition window setCompositionAreaVisible(boolean visible)154 void setCompositionAreaVisible(boolean visible) { 155 compositionWindow.setVisible(visible); 156 } 157 158 // returns true if composition area is visible isCompositionAreaVisible()159 boolean isCompositionAreaVisible() { 160 return compositionWindow.isVisible(); 161 } 162 163 // workaround for the Solaris focus lost problem 164 class FrameWindowAdapter extends WindowAdapter { windowActivated(WindowEvent e)165 public void windowActivated(WindowEvent e) { 166 requestFocus(); 167 } 168 } 169 170 // InputMethodListener methods - just forward to the current handler inputMethodTextChanged(InputMethodEvent event)171 public void inputMethodTextChanged(InputMethodEvent event) { 172 handler.inputMethodTextChanged(event); 173 } 174 caretPositionChanged(InputMethodEvent event)175 public void caretPositionChanged(InputMethodEvent event) { 176 handler.caretPositionChanged(event); 177 } 178 179 /** 180 * Sets the text and caret to be displayed in this composition area. 181 * Shows the window if it contains text, hides it if not. 182 */ setText(AttributedCharacterIterator composedText, TextHitInfo caret)183 void setText(AttributedCharacterIterator composedText, TextHitInfo caret) { 184 composedTextLayout = null; 185 if (composedText == null) { 186 // there's no composed text to display, so hide the window 187 compositionWindow.setVisible(false); 188 this.caret = null; 189 } else { 190 /* since we have composed text, make sure the window is shown. 191 This is necessary to get a valid graphics object. See 6181385. 192 */ 193 if (!compositionWindow.isVisible()) { 194 compositionWindow.setVisible(true); 195 } 196 197 Graphics g = getGraphics(); 198 199 if (g == null) { 200 return; 201 } 202 203 try { 204 updateWindowLocation(); 205 206 FontRenderContext context = ((Graphics2D)g).getFontRenderContext(); 207 composedTextLayout = new TextLayout(composedText, context); 208 Rectangle2D bounds = composedTextLayout.getBounds(); 209 210 this.caret = caret; 211 212 // Resize the composition area to just fit the text. 213 FontMetrics metrics = g.getFontMetrics(); 214 Rectangle2D maxCharBoundsRec = metrics.getMaxCharBounds(g); 215 int newHeight = (int)maxCharBoundsRec.getHeight() + HEIGHT_MARGIN; 216 int newFrameHeight = newHeight +compositionWindow.getInsets().top 217 +compositionWindow.getInsets().bottom; 218 // If it's a passive client, set the width always to PASSIVE_WIDTH (480px) 219 InputMethodRequests req = handler.getClientInputMethodRequests(); 220 int newWidth = (req==null) ? PASSIVE_WIDTH : (int)bounds.getWidth() + WIDTH_MARGIN; 221 int newFrameWidth = newWidth + compositionWindow.getInsets().left 222 + compositionWindow.getInsets().right; 223 setPreferredSize(new Dimension(newWidth, newHeight)); 224 compositionWindow.setSize(new Dimension(newFrameWidth, newFrameHeight)); 225 226 // show the composed text 227 paint(g); 228 } 229 finally { 230 g.dispose(); 231 } 232 } 233 } 234 235 /** 236 * Sets the caret to be displayed in this composition area. 237 * The text is not changed. 238 */ setCaret(TextHitInfo caret)239 void setCaret(TextHitInfo caret) { 240 this.caret = caret; 241 if (compositionWindow.isVisible()) { 242 Graphics g = getGraphics(); 243 try { 244 paint(g); 245 } finally { 246 g.dispose(); 247 } 248 } 249 } 250 251 /** 252 * Positions the composition window near (usually below) the 253 * insertion point in the client component if the client 254 * component is an active client (below-the-spot input). 255 */ updateWindowLocation()256 void updateWindowLocation() { 257 InputMethodRequests req = handler.getClientInputMethodRequests(); 258 if (req == null) { 259 // not an active client 260 return; 261 } 262 263 Point windowLocation = new Point(); 264 265 Rectangle caretRect = req.getTextLocation(null); 266 Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); 267 Dimension windowSize = compositionWindow.getSize(); 268 final int SPACING = 2; 269 270 if (caretRect.x + windowSize.width > screenSize.width) { 271 windowLocation.x = screenSize.width - windowSize.width; 272 } else { 273 windowLocation.x = caretRect.x; 274 } 275 276 if (caretRect.y + caretRect.height + SPACING + windowSize.height > screenSize.height) { 277 windowLocation.y = caretRect.y - SPACING - windowSize.height; 278 } else { 279 windowLocation.y = caretRect.y + caretRect.height + SPACING; 280 } 281 282 compositionWindow.setLocation(windowLocation); 283 } 284 285 // support for InputMethodRequests methods getTextLocation(TextHitInfo offset)286 Rectangle getTextLocation(TextHitInfo offset) { 287 Rectangle rectangle = getCaretRectangle(offset); 288 Point location = getLocationOnScreen(); 289 rectangle.translate(location.x, location.y); 290 return rectangle; 291 } 292 getLocationOffset(int x, int y)293 TextHitInfo getLocationOffset(int x, int y) { 294 TextLayout layout = composedTextLayout; 295 if (layout == null) { 296 return null; 297 } else { 298 Point location = getLocationOnScreen(); 299 x -= location.x + TEXT_ORIGIN_X; 300 y -= location.y + TEXT_ORIGIN_Y; 301 if (layout.getBounds().contains(x, y)) { 302 return layout.hitTestChar(x, y); 303 } else { 304 return null; 305 } 306 } 307 } 308 309 // Disables or enables decorations of the composition window setCompositionAreaUndecorated(boolean setUndecorated)310 void setCompositionAreaUndecorated(boolean setUndecorated){ 311 if (compositionWindow.isDisplayable()){ 312 compositionWindow.removeNotify(); 313 } 314 compositionWindow.setUndecorated(setUndecorated); 315 compositionWindow.pack(); 316 } 317 318 /** 319 * Use serialVersionUID from JDK 1.7 for interoperability. 320 */ 321 @Serial 322 private static final long serialVersionUID = -1057247068746557444L; 323 } 324