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