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