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