1 /*
   2  * Copyright (c) 2011, 2015, 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.lwawt.macosx;
  27 
  28 import java.awt.im.spi.*;
  29 import java.util.*;
  30 import java.awt.*;
  31 import java.awt.peer.*;
  32 import java.awt.event.*;
  33 import java.awt.im.*;
  34 import java.awt.font.*;
  35 import java.lang.Character.Subset;
  36 import java.lang.reflect.InvocationTargetException;
  37 import java.text.AttributedCharacterIterator.Attribute;
  38 import java.text.*;
  39 import javax.swing.text.JTextComponent;
  40 
  41 import sun.awt.AWTAccessor;
  42 import sun.awt.im.InputMethodAdapter;
  43 import sun.lwawt.*;
  44 
  45 import static sun.awt.AWTAccessor.ComponentAccessor;
  46 
  47 public class CInputMethod extends InputMethodAdapter {
  48     private InputMethodContext fIMContext;
  49     private Component fAwtFocussedComponent;
  50     private LWComponentPeer<?, ?> fAwtFocussedComponentPeer;
  51     private boolean isActive;
  52 
  53     private static Map<TextAttribute, Integer>[] sHighlightStyles;
  54 
  55     // Intitalize highlight mapping table and its mapper.
  56     static {
  57         @SuppressWarnings({"rawtypes", "unchecked"})
  58         Map<TextAttribute, Integer> styles[] = new Map[4];
  59         HashMap<TextAttribute, Integer> map;
  60 
  61         // UNSELECTED_RAW_TEXT_HIGHLIGHT
  62         map = new HashMap<TextAttribute, Integer>(1);
  63         map.put(TextAttribute.INPUT_METHOD_UNDERLINE,
  64                 TextAttribute.UNDERLINE_LOW_GRAY);
  65         styles[0] = Collections.unmodifiableMap(map);
  66 
  67         // SELECTED_RAW_TEXT_HIGHLIGHT
  68         map = new HashMap<TextAttribute, Integer>(1);
  69         map.put(TextAttribute.INPUT_METHOD_UNDERLINE,
  70                 TextAttribute.UNDERLINE_LOW_GRAY);
  71         styles[1] = Collections.unmodifiableMap(map);
  72 
  73         // UNSELECTED_CONVERTED_TEXT_HIGHLIGHT
  74         map = new HashMap<TextAttribute, Integer>(1);
  75         map.put(TextAttribute.INPUT_METHOD_UNDERLINE,
  76                 TextAttribute.UNDERLINE_LOW_ONE_PIXEL);
  77         styles[2] = Collections.unmodifiableMap(map);
  78 
  79         // SELECTED_CONVERTED_TEXT_HIGHLIGHT
  80         map = new HashMap<TextAttribute, Integer>(1);
  81         map.put(TextAttribute.INPUT_METHOD_UNDERLINE,
  82                 TextAttribute.UNDERLINE_LOW_TWO_PIXEL);
  83         styles[3] = Collections.unmodifiableMap(map);
  84 
  85         sHighlightStyles = styles;
  86 
  87         nativeInit();
  88 
  89     }
  90 
  91     public CInputMethod() {
  92     }
  93 
  94 
  95     /**
  96         * Sets the input method context, which is used to dispatch input method
  97      * events to the client component and to request information from
  98      * the client component.
  99      * <p>
 100      * This method is called once immediately after instantiating this input
 101      * method.
 102      *
 103      * @param context the input method context for this input method
 104      * @exception NullPointerException if <code>context</code> is null
 105      */
 106     public void setInputMethodContext(InputMethodContext context) {
 107         fIMContext = context;
 108     }
 109 
 110     /**
 111         * Attempts to set the input locale. If the input method supports the
 112      * desired locale, it changes its behavior to support input for the locale
 113      * and returns true.
 114      * Otherwise, it returns false and does not change its behavior.
 115      * <p>
 116      * This method is called
 117      * <ul>
 118      * <li>by {@link java.awt.im.InputContext#selectInputMethod InputContext.selectInputMethod},
 119      * <li>when switching to this input method through the user interface if the user
 120      *     specified a locale or if the previously selected input method's
 121      *     {@link java.awt.im.spi.InputMethod#getLocale getLocale} method
 122      *     returns a non-null value.
 123      * </ul>
 124      *
 125      * @param lang locale to input
 126      * @return whether the specified locale is supported
 127      * @exception NullPointerException if <code>locale</code> is null
 128      */
 129     public boolean setLocale(Locale lang) {
 130         return setLocale(lang, false);
 131     }
 132 
 133     private boolean setLocale(Locale lang, boolean onActivate) {
 134         Object[] available = CInputMethodDescriptor.getAvailableLocalesInternal();
 135         for (int i = 0; i < available.length; i++) {
 136             Locale locale = (Locale)available[i];
 137             if (lang.equals(locale) ||
 138                 // special compatibility rule for Japanese and Korean
 139                 locale.equals(Locale.JAPAN) && lang.equals(Locale.JAPANESE) ||
 140                 locale.equals(Locale.KOREA) && lang.equals(Locale.KOREAN)) {
 141                 if (isActive) {
 142                     setNativeLocale(locale.toString(), onActivate);
 143                 }
 144                 return true;
 145             }
 146         }
 147         return false;
 148     }
 149 
 150     /**
 151         * Returns the current input locale. Might return null in exceptional cases.
 152      * <p>
 153      * This method is called
 154      * <ul>
 155      * <li>by {@link java.awt.im.InputContext#getLocale InputContext.getLocale} and
 156      * <li>when switching from this input method to a different one through the
 157      *     user interface.
 158      * </ul>
 159      *
 160      * @return the current input locale, or null
 161      */
 162     public Locale getLocale() {
 163         // On Mac OS X we'll ask the currently active input method what its locale is.
 164         Locale returnValue = getNativeLocale();
 165         if (returnValue == null) {
 166             returnValue = Locale.getDefault();
 167         }
 168 
 169         return returnValue;
 170     }
 171 
 172     /**
 173         * Sets the subsets of the Unicode character set that this input method
 174      * is allowed to input. Null may be passed in to indicate that all
 175      * characters are allowed.
 176      * <p>
 177      * This method is called
 178      * <ul>
 179      * <li>immediately after instantiating this input method,
 180      * <li>when switching to this input method from a different one, and
 181      * <li>by {@link java.awt.im.InputContext#setCharacterSubsets InputContext.setCharacterSubsets}.
 182      * </ul>
 183      *
 184      * @param subsets the subsets of the Unicode character set from which
 185      * characters may be input
 186      */
 187     public void setCharacterSubsets(Subset[] subsets) {
 188         // -- SAK: Does mac OS X support this?
 189     }
 190 
 191     /**
 192         * Composition cannot be set on Mac OS X -- the input method remembers this
 193      */
 194     public void setCompositionEnabled(boolean enable) {
 195         throw new UnsupportedOperationException("Can't adjust composition mode on Mac OS X.");
 196     }
 197 
 198     public boolean isCompositionEnabled() {
 199         throw new UnsupportedOperationException("Can't adjust composition mode on Mac OS X.");
 200     }
 201 
 202     /**
 203      * Dispatches the event to the input method. If input method support is
 204      * enabled for the focussed component, incoming events of certain types
 205      * are dispatched to the current input method for this component before
 206      * they are dispatched to the component's methods or event listeners.
 207      * The input method decides whether it needs to handle the event. If it
 208      * does, it also calls the event's <code>consume</code> method; this
 209      * causes the event to not get dispatched to the component's event
 210      * processing methods or event listeners.
 211      * <p>
 212      * Events are dispatched if they are instances of InputEvent or its
 213      * subclasses.
 214      * This includes instances of the AWT classes KeyEvent and MouseEvent.
 215      * <p>
 216      * This method is called by {@link java.awt.im.InputContext#dispatchEvent InputContext.dispatchEvent}.
 217      *
 218      * @param event the event being dispatched to the input method
 219      * @exception NullPointerException if <code>event</code> is null
 220      */
 221     public void dispatchEvent(final AWTEvent event) {
 222         // No-op for Mac OS X.
 223     }
 224 
 225 
 226     /**
 227      * Activate and deactivate are no-ops on Mac OS X.
 228      * A non-US keyboard layout is an 'input method' in that it generates events the same way as
 229      * a CJK input method. A component that doesn't want input method events still wants the dead-key
 230      * events.
 231      *
 232      *
 233      */
 234     public void activate() {
 235         isActive = true;
 236     }
 237 
 238     public void deactivate(boolean isTemporary) {
 239         isActive = false;
 240     }
 241 
 242     /**
 243      * Closes or hides all windows opened by this input method instance or
 244      * its class.  Deactivate hides windows for us on Mac OS X.
 245      */
 246     public void hideWindows() {
 247     }
 248 
 249     long getNativeViewPtr(LWComponentPeer<?, ?> peer) {
 250         if (peer.getPlatformWindow() instanceof CPlatformWindow) {
 251             CPlatformWindow platformWindow = (CPlatformWindow) peer.getPlatformWindow();
 252             CPlatformView platformView = platformWindow.getContentView();
 253             return platformView.getAWTView();
 254         } else {
 255             return 0;
 256         }
 257     }
 258 
 259     /**
 260         * Notifies the input method that a client component has been
 261      * removed from its containment hierarchy, or that input method
 262      * support has been disabled for the component.
 263      */
 264     public void removeNotify() {
 265         if (fAwtFocussedComponentPeer != null) {
 266             nativeEndComposition(getNativeViewPtr(fAwtFocussedComponentPeer));
 267         }
 268 
 269         fAwtFocussedComponentPeer = null;
 270     }
 271 
 272     /**
 273      * Informs the input method adapter about the component that has the AWT
 274      * focus if it's using the input context owning this adapter instance.
 275      * We also take the opportunity to tell the native side that we are the input method
 276      * to talk to when responding to key events.
 277      */
 278     protected void setAWTFocussedComponent(Component component) {
 279         LWComponentPeer<?, ?> peer = null;
 280         long modelPtr = 0;
 281         CInputMethod imInstance = this;
 282 
 283         // component will be null when we are told there's no focused component.
 284         // When that happens we need to notify the native architecture to stop generating IMEs
 285         if (component == null) {
 286             peer = fAwtFocussedComponentPeer;
 287             imInstance = null;
 288         } else {
 289             peer = getNearestNativePeer(component);
 290 
 291             // If we have a passive client, don't pass input method events to it.
 292             if (component.getInputMethodRequests() == null) {
 293                 imInstance = null;
 294             }
 295         }
 296 
 297         if (peer != null) {
 298             modelPtr = getNativeViewPtr(peer);
 299 
 300             // modelPtr refers to the ControlModel that either got or lost focus.
 301             nativeNotifyPeer(modelPtr, imInstance);
 302         }
 303 
 304         // Track the focused component and its nearest peer.
 305         fAwtFocussedComponent = component;
 306         fAwtFocussedComponentPeer = getNearestNativePeer(component);
 307     }
 308 
 309     /**
 310         * @see java.awt.Toolkit#mapInputMethodHighlight
 311      */
 312     public static Map<TextAttribute, ?> mapInputMethodHighlight(InputMethodHighlight highlight) {
 313         int index;
 314         int state = highlight.getState();
 315         if (state == InputMethodHighlight.RAW_TEXT) {
 316             index = 0;
 317         } else if (state == InputMethodHighlight.CONVERTED_TEXT) {
 318             index = 2;
 319         } else {
 320             return null;
 321         }
 322         if (highlight.isSelected()) {
 323             index += 1;
 324         }
 325         return sHighlightStyles[index];
 326     }
 327 
 328     /**
 329         * Ends any input composition that may currently be going on in this
 330      * context. Depending on the platform and possibly user preferences,
 331      * this may commit or delete uncommitted text. Any changes to the text
 332      * are communicated to the active component using an input method event.
 333      *
 334      * <p>
 335      * A text editing component may call this in a variety of situations,
 336      * for example, when the user moves the insertion point within the text
 337      * (but outside the composed text), or when the component's text is
 338      * saved to a file or copied to the clipboard.
 339      * <p>
 340      * This method is called
 341      * <ul>
 342      * <li>by {@link java.awt.im.InputContext#endComposition InputContext.endComposition},
 343      * <li>by {@link java.awt.im.InputContext#dispatchEvent InputContext.dispatchEvent}
 344      *     when switching to a different client component
 345      * <li>when switching from this input method to a different one using the
 346      *     user interface or
 347      *     {@link java.awt.im.InputContext#selectInputMethod InputContext.selectInputMethod}.
 348      * </ul>
 349      */
 350     public void endComposition() {
 351         if (fAwtFocussedComponentPeer != null)
 352             nativeEndComposition(getNativeViewPtr(fAwtFocussedComponentPeer));
 353     }
 354 
 355     /**
 356         * Disposes of the input method and releases the resources used by it.
 357      * In particular, the input method should dispose windows and close files that are no
 358      * longer needed.
 359      * <p>
 360      * This method is called by {@link java.awt.im.InputContext#dispose InputContext.dispose}.
 361      * <p>
 362      * The method is only called when the input method is inactive.
 363      * No method of this interface is called on this instance after dispose.
 364      */
 365     public void dispose() {
 366         fIMContext = null;
 367         fAwtFocussedComponent = null;
 368         fAwtFocussedComponentPeer = null;
 369     }
 370 
 371     /**
 372         * Returns a control object from this input method, or null. A
 373      * control object provides methods that control the behavior of the
 374      * input method or obtain information from the input method. The type
 375      * of the object is an input method specific class. Clients have to
 376      * compare the result against known input method control object
 377      * classes and cast to the appropriate class to invoke the methods
 378      * provided.
 379      * <p>
 380      * This method is called by
 381      * {@link java.awt.im.InputContext#getInputMethodControlObject InputContext.getInputMethodControlObject}.
 382      *
 383      * @return a control object from this input method, or null
 384      */
 385     public Object getControlObject() {
 386         return null;
 387     }
 388 
 389     // java.awt.Toolkit#getNativeContainer() is not available
 390     //    from this package
 391     private LWComponentPeer<?, ?> getNearestNativePeer(Component comp) {
 392         if (comp==null)
 393             return null;
 394         final ComponentAccessor acc = AWTAccessor.getComponentAccessor();
 395         ComponentPeer peer = acc.getPeer(comp);
 396         if (peer==null)
 397             return null;
 398 
 399         while (peer instanceof java.awt.peer.LightweightPeer) {
 400             comp = comp.getParent();
 401             if (comp==null)
 402                 return null;
 403             peer = acc.getPeer(comp);
 404             if (peer==null)
 405                 return null;
 406         }
 407 
 408         if (peer instanceof LWComponentPeer)
 409             return (LWComponentPeer)peer;
 410 
 411         return null;
 412     }
 413 
 414     // =========================== NSTextInput callbacks ===========================
 415     // The 'marked text' that we get from Cocoa.  We need to track this separately, since
 416     // Java doesn't let us ask the IM context for it.
 417     private AttributedString fCurrentText = null;
 418     private String fCurrentTextAsString = null;
 419     private int fCurrentTextLength = 0;
 420 
 421     /**
 422      * Tell the component to commit all of the characters in the string to the current
 423      * text view. This effectively wipes out any text in progress.
 424      */
 425     synchronized private void insertText(String aString) {
 426         AttributedString attribString = new AttributedString(aString);
 427 
 428         // Set locale information on the new string.
 429         attribString.addAttribute(Attribute.LANGUAGE, getLocale(), 0, aString.length());
 430 
 431         TextHitInfo theCaret = TextHitInfo.afterOffset(aString.length() - 1);
 432         InputMethodEvent event = new InputMethodEvent(fAwtFocussedComponent,
 433                                                       InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
 434                                                       attribString.getIterator(),
 435                                                       aString.length(),
 436                                                       theCaret,
 437                                                       theCaret);
 438         LWCToolkit.postEvent(LWCToolkit.targetToAppContext(fAwtFocussedComponent), event);
 439         fCurrentText = null;
 440         fCurrentTextAsString = null;
 441         fCurrentTextLength = 0;
 442     }
 443 
 444     private void startIMUpdate (String rawText) {
 445         fCurrentTextAsString = new String(rawText);
 446         fCurrentText = new AttributedString(fCurrentTextAsString);
 447         fCurrentTextLength = rawText.length();
 448     }
 449 
 450     static private final int kCaretPosition = 0;
 451     static private final int kRawText = 1;
 452     static private final int kSelectedRawText = 2;
 453     static private final int kConvertedText = 3;
 454     static private final int kSelectedConvertedText = 4;
 455 
 456     /**
 457      * Convert Cocoa text highlight attributes into Java input method highlighting.
 458      */
 459     private void addAttribute (boolean isThickUnderline, boolean isGray, int start, int length) {
 460         int begin = start;
 461         int end = start + length;
 462         int markupType = kRawText;
 463 
 464         if (isThickUnderline && isGray) {
 465             markupType = kRawText;
 466         } else if (!isThickUnderline && isGray) {
 467             markupType = kRawText;
 468         } else if (isThickUnderline && !isGray) {
 469             markupType = kSelectedConvertedText;
 470         } else if (!isThickUnderline && !isGray) {
 471             markupType = kConvertedText;
 472         }
 473 
 474         InputMethodHighlight theHighlight;
 475 
 476         switch (markupType) {
 477             case kSelectedRawText:
 478                 theHighlight = InputMethodHighlight.SELECTED_RAW_TEXT_HIGHLIGHT;
 479                 break;
 480             case kConvertedText:
 481                 theHighlight = InputMethodHighlight.UNSELECTED_CONVERTED_TEXT_HIGHLIGHT;
 482                 break;
 483             case kSelectedConvertedText:
 484                 theHighlight = InputMethodHighlight.SELECTED_CONVERTED_TEXT_HIGHLIGHT;
 485                 break;
 486             case kRawText:
 487             default:
 488                 theHighlight = InputMethodHighlight.UNSELECTED_RAW_TEXT_HIGHLIGHT;
 489                 break;
 490         }
 491 
 492         fCurrentText.addAttribute(TextAttribute.INPUT_METHOD_HIGHLIGHT, theHighlight, begin, end);
 493     }
 494 
 495    /* Called from JNI to select the previously typed glyph during press and hold */
 496     private void selectPreviousGlyph() {
 497         if (fIMContext == null) return; // ???
 498         try {
 499             LWCToolkit.invokeLater(new Runnable() {
 500                 public void run() {
 501                     final int offset = fIMContext.getInsertPositionOffset();
 502                     if (offset < 1) return; // ???
 503 
 504                     if (fAwtFocussedComponent instanceof JTextComponent) {
 505                         ((JTextComponent) fAwtFocussedComponent).select(offset - 1, offset);
 506                         return;
 507                     }
 508 
 509                     if (fAwtFocussedComponent instanceof TextComponent) {
 510                         ((TextComponent) fAwtFocussedComponent).select(offset - 1, offset);
 511                         return;
 512                     }
 513                     // TODO: Ideally we want to disable press-and-hold in this case
 514                 }
 515             }, fAwtFocussedComponent);
 516         } catch (Exception e) {
 517             e.printStackTrace();
 518         }
 519     }
 520 
 521     private void selectNextGlyph() {
 522         if (fIMContext == null || !(fAwtFocussedComponent instanceof JTextComponent)) return;
 523         try {
 524             LWCToolkit.invokeLater(new Runnable() {
 525                 public void run() {
 526                     final int offset = fIMContext.getInsertPositionOffset();
 527                     if (offset < 0) return;
 528                     ((JTextComponent) fAwtFocussedComponent).select(offset, offset + 1);
 529                     return;
 530                 }
 531             }, fAwtFocussedComponent);
 532         } catch (Exception e) {
 533             e.printStackTrace();
 534         }
 535     }
 536 
 537     private void dispatchText(int selectStart, int selectLength, boolean pressAndHold) {
 538         // Nothing to do if we have no text.
 539         if (fCurrentText == null)
 540             return;
 541 
 542         TextHitInfo theCaret = (selectLength == 0 ? TextHitInfo.beforeOffset(selectStart) : null);
 543         TextHitInfo visiblePosition = TextHitInfo.beforeOffset(0);
 544 
 545         InputMethodEvent event = new InputMethodEvent(fAwtFocussedComponent,
 546                                                       InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
 547                                                       fCurrentText.getIterator(),
 548                                                       0,
 549                                                       theCaret,
 550                                                       visiblePosition);
 551         LWCToolkit.postEvent(LWCToolkit.targetToAppContext(fAwtFocussedComponent), event);
 552 
 553         if (pressAndHold) selectNextGlyph();
 554     }
 555 
 556     /**
 557      * Frequent callbacks from NSTextInput.  I think we're supposed to commit it here?
 558      */
 559     synchronized private void unmarkText() {
 560         if (fCurrentText == null)
 561             return;
 562 
 563         TextHitInfo theCaret = TextHitInfo.afterOffset(fCurrentTextLength);
 564         TextHitInfo visiblePosition = theCaret;
 565         InputMethodEvent event = new InputMethodEvent(fAwtFocussedComponent,
 566                                                       InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
 567                                                       fCurrentText.getIterator(),
 568                                                       fCurrentTextLength,
 569                                                       theCaret,
 570                                                       visiblePosition);
 571         LWCToolkit.postEvent(LWCToolkit.targetToAppContext(fAwtFocussedComponent), event);
 572         fCurrentText = null;
 573         fCurrentTextAsString = null;
 574         fCurrentTextLength = 0;
 575     }
 576 
 577     synchronized private boolean hasMarkedText() {
 578         return fCurrentText != null;
 579     }
 580 
 581     /**
 582         * Cocoa assumes the marked text and committed text is all stored in the same storage, but
 583      * Java does not.  So, we have to see where the request is and based on that return the right
 584      * substring.
 585      */
 586     synchronized private String attributedSubstringFromRange(final int locationIn, final int lengthIn) {
 587         final String[] retString = new String[1];
 588 
 589         try {
 590             LWCToolkit.invokeAndWait(new Runnable() {
 591                 public void run() { synchronized(retString) {
 592                     int location = locationIn;
 593                     int length = lengthIn;
 594 
 595                     if ((location + length) > (fIMContext.getCommittedTextLength() + fCurrentTextLength)) {
 596                         length = fIMContext.getCommittedTextLength() - location;
 597                     }
 598 
 599                     AttributedCharacterIterator theIterator = null;
 600 
 601                     if (fCurrentText == null) {
 602                         theIterator = fIMContext.getCommittedText(location, location + length, null);
 603                     } else {
 604                         int insertSpot = fIMContext.getInsertPositionOffset();
 605 
 606                         if (location < insertSpot) {
 607                             theIterator = fIMContext.getCommittedText(location, location + length, null);
 608                         } else if (location >= insertSpot && location < insertSpot + fCurrentTextLength) {
 609                             theIterator = fCurrentText.getIterator(null, location - insertSpot, location - insertSpot +length);
 610                         } else  {
 611                             theIterator = fIMContext.getCommittedText(location - fCurrentTextLength, location - fCurrentTextLength + length, null);
 612                         }
 613                     }
 614 
 615                     // Get the characters from the iterator
 616                     char selectedText[] = new char[theIterator.getEndIndex() - theIterator.getBeginIndex()];
 617                     char current = theIterator.first();
 618                     int index = 0;
 619                     while (current != CharacterIterator.DONE) {
 620                         selectedText[index++] = current;
 621                         current = theIterator.next();
 622                     }
 623 
 624                     retString[0] = new String(selectedText);
 625                 }}
 626             }, fAwtFocussedComponent);
 627         } catch (InvocationTargetException ite) { ite.printStackTrace(); }
 628 
 629         synchronized(retString) { return retString[0]; }
 630     }
 631 
 632     /**
 633      * Cocoa wants the range of characters that are currently selected.  We have to synthesize this
 634      * by getting the insert location and the length of the selected text. NB:  This does NOT allow
 635      * for the fact that the insert point in Swing can come AFTER the selected text, making this
 636      * potentially incorrect.
 637      */
 638     synchronized private int[] selectedRange() {
 639         final int[] returnValue = new int[2];
 640 
 641         try {
 642             LWCToolkit.invokeAndWait(new Runnable() {
 643                 public void run() { synchronized(returnValue) {
 644                     AttributedCharacterIterator theIterator = fIMContext.getSelectedText(null);
 645                     if (theIterator == null) {
 646                         returnValue[0] = fIMContext.getInsertPositionOffset();
 647                         returnValue[1] = 0;
 648                         return;
 649                     }
 650 
 651                     int startLocation;
 652 
 653                     if (fAwtFocussedComponent instanceof JTextComponent) {
 654                         JTextComponent theComponent = (JTextComponent)fAwtFocussedComponent;
 655                         startLocation = theComponent.getSelectionStart();
 656                     } else if (fAwtFocussedComponent instanceof TextComponent) {
 657                         TextComponent theComponent = (TextComponent)fAwtFocussedComponent;
 658                         startLocation = theComponent.getSelectionStart();
 659                     } else {
 660                         // If we don't have a Swing or AWT component, we have to guess whether the selection is before or after the input spot.
 661                         startLocation = fIMContext.getInsertPositionOffset() - (theIterator.getEndIndex() - theIterator.getBeginIndex());
 662 
 663                         // If the calculated spot is negative the insert spot must be at the beginning of
 664                         // the selection.
 665                         if (startLocation <  0) {
 666                             startLocation = fIMContext.getInsertPositionOffset() + (theIterator.getEndIndex() - theIterator.getBeginIndex());
 667                         }
 668                     }
 669 
 670                     returnValue[0] = startLocation;
 671                     returnValue[1] = theIterator.getEndIndex() - theIterator.getBeginIndex();
 672 
 673                 }}
 674             }, fAwtFocussedComponent);
 675         } catch (InvocationTargetException ite) { ite.printStackTrace(); }
 676 
 677         synchronized(returnValue) { return returnValue; }
 678     }
 679 
 680     /**
 681      * Cocoa wants the range of characters that are currently marked.  Since Java doesn't store committed and
 682      * text in progress (composed text) together, we have to synthesize it.  We know where the text will be
 683      * inserted, so we can return that position, and the length of the text in progress.  If there is no marked text
 684      * return null.
 685      */
 686     synchronized private int[] markedRange() {
 687         if (fCurrentText == null)
 688             return null;
 689 
 690         final int[] returnValue = new int[2];
 691 
 692         try {
 693             LWCToolkit.invokeAndWait(new Runnable() {
 694                 public void run() { synchronized(returnValue) {
 695                     // The insert position is always after the composed text, so the range start is the
 696                     // insert spot less the length of the composed text.
 697                     returnValue[0] = fIMContext.getInsertPositionOffset();
 698                 }}
 699             }, fAwtFocussedComponent);
 700         } catch (InvocationTargetException ite) { ite.printStackTrace(); }
 701 
 702         returnValue[1] = fCurrentTextLength;
 703         synchronized(returnValue) { return returnValue; }
 704     }
 705 
 706     /**
 707      * Cocoa wants a rectangle that describes where a particular range is on screen, but only cares about the
 708      * location of that rectangle.  We are given the index of the character for which we want the location on
 709      * screen, which will be a character in the in-progress text.  By subtracting the current insert position,
 710      * which is always in front of the in-progress text, we get the offset into the composed text, and we get
 711      * that location from the input method context.
 712      */
 713     synchronized private int[] firstRectForCharacterRange(final int absoluteTextOffset) {
 714         final int[] rect = new int[4];
 715 
 716         try {
 717             LWCToolkit.invokeAndWait(new Runnable() {
 718                 public void run() { synchronized(rect) {
 719                     int insertOffset = fIMContext.getInsertPositionOffset();
 720                     int composedTextOffset = absoluteTextOffset - insertOffset;
 721                     if (composedTextOffset < 0) composedTextOffset = 0;
 722                     Rectangle r = fIMContext.getTextLocation(TextHitInfo.beforeOffset(composedTextOffset));
 723                     rect[0] = r.x;
 724                     rect[1] = r.y;
 725                     rect[2] = r.width;
 726                     rect[3] = r.height;
 727 
 728                     // This next if-block is a hack to work around a bug in JTextComponent. getTextLocation ignores
 729                     // the TextHitInfo passed to it and always returns the location of the insertion point, which is
 730                     // at the start of the composed text.  We'll do some calculation so the candidate window for Kotoeri
 731                     // follows the requested offset into the composed text.
 732                     if (composedTextOffset > 0 && (fAwtFocussedComponent instanceof JTextComponent)) {
 733                         Rectangle r2 = fIMContext.getTextLocation(TextHitInfo.beforeOffset(0));
 734 
 735                         if (r.equals(r2)) {
 736                             // FIXME: (SAK) If the candidate text wraps over two lines, this calculation pushes the candidate
 737                             // window off the right edge of the component.
 738                             String inProgressSubstring = fCurrentTextAsString.substring(0, composedTextOffset);
 739                             Graphics g = fAwtFocussedComponent.getGraphics();
 740                             int xOffset = g.getFontMetrics().stringWidth(inProgressSubstring);
 741                             rect[0] += xOffset;
 742                             g.dispose();
 743                         }
 744                     }
 745                 }}
 746             }, fAwtFocussedComponent);
 747         } catch (InvocationTargetException ite) { ite.printStackTrace(); }
 748 
 749         synchronized(rect) { return rect; }
 750     }
 751 
 752     /* This method returns the index for the character that is nearest to the point described by screenX and screenY.
 753      * The coordinates are in Java screen coordinates.  If no character in the composed text was hit, we return -1, indicating
 754      * not found.
 755      */
 756     synchronized private int characterIndexForPoint(final int screenX, final int screenY) {
 757         final TextHitInfo[] offsetInfo = new TextHitInfo[1];
 758         final int[] insertPositionOffset = new int[1];
 759 
 760         try {
 761             LWCToolkit.invokeAndWait(new Runnable() {
 762                 public void run() { synchronized(offsetInfo) {
 763                     offsetInfo[0] = fIMContext.getLocationOffset(screenX, screenY);
 764                     insertPositionOffset[0] = fIMContext.getInsertPositionOffset();
 765                 }}
 766             }, fAwtFocussedComponent);
 767         } catch (InvocationTargetException ite) { ite.printStackTrace(); }
 768 
 769         // This bit of gymnastics ensures that the returned location is within the composed text.
 770         // If it falls outside that region, the input method will commit the text, which is inconsistent with native
 771         // Cocoa apps (see TextEdit, for example.)  Clicking to the left of or above the selected text moves the
 772         // cursor to the start of the composed text, and to the right or below moves it to one character before the end.
 773         if (offsetInfo[0] == null) {
 774             return insertPositionOffset[0];
 775         }
 776 
 777         int returnValue = offsetInfo[0].getCharIndex() + insertPositionOffset[0];
 778 
 779         if (offsetInfo[0].getCharIndex() == fCurrentTextLength)
 780             returnValue --;
 781 
 782         return returnValue;
 783     }
 784 
 785     // On Mac OS X we effectively disabled the input method when focus was lost, so
 786     // this call can be ignored.
 787     public void disableInputMethod()
 788     {
 789         // Deliberately ignored. See setAWTFocussedComponent above.
 790     }
 791 
 792     public String getNativeInputMethodInfo()
 793     {
 794         return nativeGetCurrentInputMethodInfo();
 795     }
 796 
 797 
 798     // =========================== Native methods ===========================
 799     // Note that if nativePeer isn't something that normally accepts keystrokes (i.e., a CPanel)
 800     // these calls will be ignored.
 801     private native void nativeNotifyPeer(long nativePeer, CInputMethod imInstance);
 802     private native void nativeEndComposition(long nativePeer);
 803     private native void nativeHandleEvent(LWComponentPeer<?, ?> peer, AWTEvent event);
 804 
 805     // Returns the locale of the active input method.
 806     static native Locale getNativeLocale();
 807 
 808     // Switches to the input method with language indicated in localeName
 809     static native boolean setNativeLocale(String localeName, boolean onActivate);
 810 
 811     // Returns information about the currently selected input method.
 812     static native String nativeGetCurrentInputMethodInfo();
 813 
 814     // Initialize toolbox routines
 815     static native void nativeInit();
 816 }