1 /*
2  * Copyright (c) 2011, 2018, 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);
map.put(TextAttribute.INPUT_METHOD_UNDERLINE, TextAttribute.UNDERLINE_LOW_GRAY)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);
map.put(TextAttribute.INPUT_METHOD_UNDERLINE, TextAttribute.UNDERLINE_LOW_GRAY)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);
map.put(TextAttribute.INPUT_METHOD_UNDERLINE, TextAttribute.UNDERLINE_LOW_ONE_PIXEL)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);
map.put(TextAttribute.INPUT_METHOD_UNDERLINE, TextAttribute.UNDERLINE_LOW_TWO_PIXEL)81         map.put(TextAttribute.INPUT_METHOD_UNDERLINE,
82                 TextAttribute.UNDERLINE_LOW_TWO_PIXEL);
83         styles[3] = Collections.unmodifiableMap(map);
84 
85         sHighlightStyles = styles;
86 
nativeInit()87         nativeInit();
88 
89     }
90 
CInputMethod()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      */
setInputMethodContext(InputMethodContext context)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      */
setLocale(Locale lang)129     public boolean setLocale(Locale lang) {
130         return setLocale(lang, false);
131     }
132 
setLocale(Locale lang, boolean onActivate)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      */
getLocale()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      */
setCharacterSubsets(Subset[] subsets)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      */
setCompositionEnabled(boolean enable)194     public void setCompositionEnabled(boolean enable) {
195         throw new UnsupportedOperationException("Can't adjust composition mode on Mac OS X.");
196     }
197 
isCompositionEnabled()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      */
dispatchEvent(final AWTEvent event)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      */
activate()234     public void activate() {
235         isActive = true;
236     }
237 
deactivate(boolean isTemporary)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      */
hideWindows()246     public void hideWindows() {
247     }
248 
getNativeViewPtr(LWComponentPeer<?, ?> peer)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      */
removeNotify()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      */
setAWTFocussedComponent(Component component)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      */
mapInputMethodHighlight(InputMethodHighlight highlight)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      */
endComposition()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      */
dispose()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      */
getControlObject()385     public Object getControlObject() {
386         return null;
387     }
388 
389     // java.awt.Toolkit#getNativeContainer() is not available
390     //    from this package
getNearestNativePeer(Component comp)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      */
insertText(String aString)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 
startIMUpdate(String rawText)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      */
addAttribute(boolean isThickUnderline, boolean isGray, int start, int length)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 */
selectPreviousGlyph()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 
selectNextGlyph()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 
dispatchText(int selectStart, int selectLength, boolean pressAndHold)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      */
unmarkText()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 
hasMarkedText()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      */
attributedSubstringFromRange(final int locationIn, final int lengthIn)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      */
selectedRange()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      */
markedRange()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      */
firstRectForCharacterRange(final int absoluteTextOffset)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      */
characterIndexForPoint(final int screenX, final int screenY)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.
disableInputMethod()787     public void disableInputMethod()
788     {
789         // Deliberately ignored. See setAWTFocussedComponent above.
790     }
791 
getNativeInputMethodInfo()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.
nativeNotifyPeer(long nativePeer, CInputMethod imInstance)801     private native void nativeNotifyPeer(long nativePeer, CInputMethod imInstance);
nativeEndComposition(long nativePeer)802     private native void nativeEndComposition(long nativePeer);
nativeHandleEvent(LWComponentPeer<?, ?> peer, AWTEvent event)803     private native void nativeHandleEvent(LWComponentPeer<?, ?> peer, AWTEvent event);
804 
805     // Returns the locale of the active input method.
getNativeLocale()806     static native Locale getNativeLocale();
807 
808     // Switches to the input method with language indicated in localeName
setNativeLocale(String localeName, boolean onActivate)809     static native boolean setNativeLocale(String localeName, boolean onActivate);
810 
811     // Returns information about the currently selected input method.
nativeGetCurrentInputMethodInfo()812     static native String nativeGetCurrentInputMethodInfo();
813 
814     // Initialize toolbox routines
nativeInit()815     static native void nativeInit();
816 }
817