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
26package sun.lwawt.macosx;
27
28import java.awt.im.spi.*;
29import java.util.*;
30import java.awt.*;
31import java.awt.peer.*;
32import java.awt.event.*;
33import java.awt.im.*;
34import java.awt.font.*;
35import java.lang.Character.Subset;
36import java.lang.reflect.InvocationTargetException;
37import java.text.AttributedCharacterIterator.Attribute;
38import java.text.*;
39import javax.swing.text.JTextComponent;
40
41import sun.awt.AWTAccessor;
42import sun.awt.im.InputMethodAdapter;
43import sun.lwawt.*;
44
45import static sun.awt.AWTAccessor.ComponentAccessor;
46
47public 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} 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} 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} 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} 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    private synchronized 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    private static final int kCaretPosition = 0;
451    private static final int kRawText = 1;
452    private static final int kSelectedRawText = 2;
453    private static final int kConvertedText = 3;
454    private static 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    private synchronized 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    private synchronized 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    private synchronized 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    private synchronized 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    private synchronized 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    private synchronized 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    private synchronized 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}
817