1 /*
   2  * Copyright (c) 1998, 2012, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package sun.awt.im;
  27 
  28 import java.awt.AWTException;
  29 import java.awt.CheckboxMenuItem;
  30 import java.awt.Component;
  31 import java.awt.Dialog;
  32 import java.awt.EventQueue;
  33 import java.awt.Frame;
  34 import java.awt.PopupMenu;
  35 import java.awt.Menu;
  36 import java.awt.MenuItem;
  37 import java.awt.Toolkit;
  38 import sun.awt.AppContext;
  39 import java.awt.event.ActionEvent;
  40 import java.awt.event.ActionListener;
  41 import java.awt.event.InvocationEvent;
  42 import java.awt.im.spi.InputMethodDescriptor;
  43 import java.lang.reflect.InvocationTargetException;
  44 import java.security.AccessController;
  45 import java.security.PrivilegedAction;
  46 import java.security.PrivilegedActionException;
  47 import java.security.PrivilegedExceptionAction;
  48 import java.util.Hashtable;
  49 import java.util.Iterator;
  50 import java.util.Locale;
  51 import java.util.ServiceLoader;
  52 import java.util.Vector;
  53 import java.util.Set;
  54 import java.util.prefs.BackingStoreException;
  55 import java.util.prefs.Preferences;
  56 import sun.awt.InputMethodSupport;
  57 import sun.awt.SunToolkit;
  58 import javax.swing.JDialog;
  59 import javax.swing.JFrame;
  60 import javax.swing.JRootPane;
  61 import java.awt.KeyboardFocusManager;
  62 
  63 /**
  64  * {@code ExecutableInputMethodManager} is the implementation of the
  65  * {@code InputMethodManager} class. It is runnable as a separate
  66  * thread in the AWT environment. 
  67  * {@code InputMethodManager.getInstance()} creates an instance of
  68  * {@code ExecutableInputMethodManager} and executes it as a deamon
  69  * thread.
  70  *
  71  * @see InputMethodManager
  72  */
  73 class ExecutableInputMethodManager extends InputMethodManager
  74                                    implements Runnable
  75 {
  76     // the input context that's informed about selections from the user interface
  77     private InputContext currentInputContext;
  78 
  79     // Menu item string for the trigger menu.
  80     private String triggerMenuString;
  81 
  82     // popup menu for selecting an input method
  83     private InputMethodPopupMenu selectionMenu;
  84     private static String selectInputMethodMenuTitle;
  85 
  86     // locator and name of host adapter
  87     private InputMethodLocator hostAdapterLocator;
  88 
  89     // locators for Java input methods
  90     private int javaInputMethodCount;         // number of Java input methods found
  91     private Vector<InputMethodLocator> javaInputMethodLocatorList;
  92 
  93     // component that is requesting input method switch
  94     // must be Frame or Dialog
  95     private Component requestComponent;
  96     private Component invokerComponent;
  97 
  98     // input context that is requesting input method switch
  99     private InputContext requestInputContext;
 100 
 101     // IM preference stuff
 102     private static final String preferredIMNode = "/sun/awt/im/preferredInputMethod";
 103     private static final String descriptorKey = "descriptor";
 104     private Hashtable<String, InputMethodLocator> preferredLocatorCache = new Hashtable<>();
 105     private Preferences userRoot;
 106 
 107     ExecutableInputMethodManager() {
 108 
 109         // set up host adapter locator
 110         Toolkit toolkit = Toolkit.getDefaultToolkit();
 111         try {
 112             if (toolkit instanceof InputMethodSupport) {
 113                 InputMethodDescriptor hostAdapterDescriptor =
 114                     ((InputMethodSupport)toolkit)
 115                     .getInputMethodAdapterDescriptor();
 116                 if (hostAdapterDescriptor != null) {
 117                     hostAdapterLocator = new InputMethodLocator(hostAdapterDescriptor, null, null);
 118                 }
 119             }
 120         } catch (AWTException e) {
 121             // if we can't get a descriptor, we'll just have to do without native input methods
 122         }
 123 
 124         javaInputMethodLocatorList = new Vector<InputMethodLocator>();
 125         initializeInputMethodLocatorList();
 126     }
 127 
 128     synchronized void initialize() {
 129         selectInputMethodMenuTitle = Toolkit.getProperty("AWT.InputMethodSelectionMenu", "Select Input Method");
 130 
 131         triggerMenuString = selectInputMethodMenuTitle;
 132     }
 133 
 134     public void run() {
 135         // If there are no multiple input methods to choose from, wait forever
 136         while (!hasMultipleInputMethods()) {
 137             try {
 138                 synchronized (this) {
 139                     wait();
 140                 }
 141             } catch (InterruptedException e) {
 142             }
 143         }
 144 
 145         // Loop for processing input method change requests
 146         while (true) {
 147             waitForChangeRequest();
 148             initializeInputMethodLocatorList();
 149             try {
 150                 if (requestComponent != null) {
 151                     showInputMethodMenuOnRequesterEDT(requestComponent);
 152                 } else {
 153                     // show the popup menu within the event thread
 154                     EventQueue.invokeAndWait(new Runnable() {
 155                         public void run() {
 156                             showInputMethodMenu();
 157                         }
 158                     });
 159                 }
 160             } catch (InterruptedException ie) {
 161             } catch (InvocationTargetException ite) {
 162                 // should we do anything under these exceptions?
 163             }
 164         }
 165     }
 166 
 167     // Shows Input Method Menu on the EDT of requester component
 168     // to avoid side effects. See 6544309.
 169     private void showInputMethodMenuOnRequesterEDT(Component requester)
 170         throws InterruptedException, InvocationTargetException {
 171 
 172         if (requester == null){
 173             return;
 174         }
 175 
 176         class AWTInvocationLock {}
 177         Object lock = new AWTInvocationLock();
 178 
 179         InvocationEvent event =
 180                 new InvocationEvent(requester,
 181                                     new Runnable() {
 182                                         public void run() {
 183                                             showInputMethodMenu();
 184                                         }
 185                                     },
 186                                     lock,
 187                                     true);
 188 
 189         AppContext requesterAppContext = SunToolkit.targetToAppContext(requester);
 190         synchronized (lock) {
 191             SunToolkit.postEvent(requesterAppContext, event);
 192             while (!event.isDispatched()) {
 193                 lock.wait();
 194             }
 195         }
 196 
 197         Throwable eventThrowable = event.getThrowable();
 198         if (eventThrowable != null) {
 199             throw new InvocationTargetException(eventThrowable);
 200         }
 201     }
 202 
 203     void setInputContext(InputContext inputContext) {
 204         if (currentInputContext != null && inputContext != null) {
 205             // don't throw this exception until 4237852 is fixed
 206             // throw new IllegalStateException("Can't have two active InputContext at the same time");
 207         }
 208         currentInputContext = inputContext;
 209     }
 210 
 211     public synchronized void notifyChangeRequest(Component comp) {
 212         if (!(comp instanceof Frame || comp instanceof Dialog))
 213             return;
 214 
 215         // if busy with the current request, ignore this request.
 216         if (requestComponent != null)
 217             return;
 218 
 219         requestComponent = comp;
 220         if (comp instanceof JDialog) {
 221             if (!(comp.getParent() instanceof JFrame))
 222                 invokerComponent = comp;
 223         }
 224         if (invokerComponent==null) {
 225             invokerComponent = KeyboardFocusManager.getCurrentKeyboardFocusManager()
 226                                                    .getFocusOwner();
 227             if (invokerComponent !=null && !invokerComponent.isShowing())
 228                 invokerComponent = comp;
 229         }
 230         if (invokerComponent==null)
 231             invokerComponent = comp;
 232         notify();
 233     }
 234 
 235     public synchronized void notifyChangeRequestByHotKey(Component comp) {
 236         invokerComponent = comp;
 237         while (!(comp instanceof Frame || comp instanceof Dialog)) {
 238             if (comp == null) {
 239                 // no Frame or Dialog found in containment hierarchy.
 240                 return;
 241             }
 242             comp = comp.getParent();
 243         }
 244 
 245         notifyChangeRequest(comp);
 246     }
 247 
 248     public String getTriggerMenuString() {
 249         return triggerMenuString;
 250     }
 251 
 252     /*
 253      * Returns true if the environment indicates there are multiple input methods
 254      */
 255     boolean hasMultipleInputMethods() {
 256         return ((hostAdapterLocator != null) && (javaInputMethodCount > 0)
 257                 || (javaInputMethodCount > 1));
 258     }
 259 
 260     private synchronized void waitForChangeRequest() {
 261         try {
 262             while (requestComponent == null) {
 263                 wait();
 264             }
 265         } catch (InterruptedException e) {
 266         }
 267     }
 268 
 269     /*
 270      * initializes the input method locator list for all
 271      * installed input method descriptors.
 272      */
 273     private void initializeInputMethodLocatorList() {
 274         synchronized (javaInputMethodLocatorList) {
 275             javaInputMethodLocatorList.clear();
 276             try {
 277                 AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
 278                     public Object run() {
 279                         for (InputMethodDescriptor descriptor :
 280                             ServiceLoader.load(InputMethodDescriptor.class,
 281                                                ClassLoader.getSystemClassLoader())) {
 282                             ClassLoader cl = descriptor.getClass().getClassLoader();
 283                             javaInputMethodLocatorList.add(new InputMethodLocator(descriptor, cl, null));
 284                         }
 285                         return null;
 286                     }
 287                 });
 288             }  catch (PrivilegedActionException e) {
 289                 e.printStackTrace();
 290             }
 291             javaInputMethodCount = javaInputMethodLocatorList.size();
 292         }
 293 
 294         if (hasMultipleInputMethods()) {
 295             // initialize preferences
 296             if (userRoot == null) {
 297                 userRoot = getUserRoot();
 298             }
 299         } else {
 300             // indicate to clients not to offer the menu
 301             triggerMenuString = null;
 302         }
 303     }
 304 
 305     private void showInputMethodMenu() {
 306 
 307         if (!hasMultipleInputMethods()) {
 308             requestComponent = null;
 309             invokerComponent = null;
 310             return;
 311         }
 312 
 313         // initialize pop-up menu
 314         if (requestComponent != null && requestComponent.equals(invokerComponent)) {
 315             selectionMenu = InputMethodPopupMenu.getAWTInstance(requestComponent,
 316                                                                 selectInputMethodMenuTitle);
 317         } else {
 318             selectionMenu = InputMethodPopupMenu.getInstance(requestComponent,
 319                                                              selectInputMethodMenuTitle);
 320         }
 321         if (invokerComponent instanceof JRootPane) {
 322             if (selectionMenu.isVisible()) {
 323                 synchronized (this) {
 324                     requestComponent = null;
 325                     invokerComponent = null;
 326                 }
 327                 return;
 328             }
 329         }
 330 
 331         // we have to rebuild the menu each time because
 332         // some input methods (such as IIIMP) may change
 333         // their list of supported locales dynamically
 334         selectionMenu.removeAll();
 335 
 336         // get information about the currently selected input method
 337         // ??? if there's no current input context, what's the point
 338         // of showing the menu?
 339         String currentSelection = getCurrentSelection();
 340 
 341         // Add menu item for host adapter
 342         if (hostAdapterLocator != null) {
 343             selectionMenu.addOneInputMethodToMenu(hostAdapterLocator, currentSelection);
 344             selectionMenu.addSeparator();
 345         }
 346 
 347         // Add menu items for other input methods
 348         for (int i = 0; i < javaInputMethodLocatorList.size(); i++) {
 349             InputMethodLocator locator = javaInputMethodLocatorList.get(i);
 350             selectionMenu.addOneInputMethodToMenu(locator, currentSelection);
 351         }
 352 
 353         synchronized (this) {
 354             if (requestComponent == null){
 355                 requestInputContext = currentInputContext;
 356                 selectionMenu.show(requestComponent, 0, 0);
 357             } else {
 358                 int offsetX = 60;
 359                 int offsetY = 80;
 360                 selectionMenu.addToComponent(requestComponent);
 361                 requestInputContext = currentInputContext;
 362                 if (requestComponent.getSize().width < offsetX * 2)
 363                     offsetX = requestComponent.getSize().width / 2;
 364                 if (requestComponent.getSize().height < offsetY * 2)
 365                     offsetY = requestComponent.getSize().height / 2;
 366                 selectionMenu.show(requestComponent, offsetX, offsetY);
 367             }
 368             requestComponent = null;
 369             invokerComponent = null;
 370 
 371         }
 372     }
 373 
 374     private String getCurrentSelection() {
 375         InputContext inputContext = currentInputContext;
 376         if (inputContext != null) {
 377             InputMethodLocator locator = inputContext.getInputMethodLocator();
 378             if (locator != null) {
 379                 return locator.getActionCommandString();
 380             }
 381         }
 382         return null;
 383     }
 384 
 385     synchronized void changeInputMethod(String choice) {
 386         InputMethodLocator locator = null;
 387 
 388         String inputMethodName = choice;
 389         String localeString = null;
 390         int index = choice.indexOf('\n');
 391         if (index != -1) {
 392             localeString = choice.substring(index + 1);
 393             inputMethodName = choice.substring(0, index);
 394         }
 395         if (hostAdapterLocator.getActionCommandString().equals(inputMethodName)) {
 396             locator = hostAdapterLocator;
 397         } else {
 398             for (int i = 0; i < javaInputMethodLocatorList.size(); i++) {
 399                 InputMethodLocator candidate = javaInputMethodLocatorList.get(i);
 400                 String name = candidate.getActionCommandString();
 401                 if (name.equals(inputMethodName)) {
 402                     locator = candidate;
 403                     break;
 404                 }
 405             }
 406         }
 407 
 408         if (locator != null && localeString != null) {
 409             String language = "", country = "", variant = "";
 410             int postIndex = localeString.indexOf('_');
 411             if (postIndex == -1) {
 412                 language = localeString;
 413             } else {
 414                 language = localeString.substring(0, postIndex);
 415                 int preIndex = postIndex + 1;
 416                 postIndex = localeString.indexOf('_', preIndex);
 417                 if (postIndex == -1) {
 418                     country = localeString.substring(preIndex);
 419                 } else {
 420                     country = localeString.substring(preIndex, postIndex);
 421                     variant = localeString.substring(postIndex + 1);
 422                 }
 423             }
 424             Locale locale = new Locale(language, country, variant);
 425             locator = locator.deriveLocator(locale);
 426         }
 427 
 428         if (locator == null)
 429             return;
 430 
 431         // tell the input context about the change
 432         if (requestInputContext != null) {
 433             requestInputContext.changeInputMethod(locator);
 434             requestInputContext = null;
 435 
 436             // remember the selection
 437             putPreferredInputMethod(locator);
 438         }
 439     }
 440 
 441     InputMethodLocator findInputMethod(Locale locale) {
 442         // look for preferred input method first
 443         InputMethodLocator locator = getPreferredInputMethod(locale);
 444         if (locator != null) {
 445             return locator;
 446         }
 447 
 448         if (hostAdapterLocator != null && hostAdapterLocator.isLocaleAvailable(locale)) {
 449             return hostAdapterLocator.deriveLocator(locale);
 450         }
 451 
 452         // Update the locator list
 453         initializeInputMethodLocatorList();
 454 
 455         for (int i = 0; i < javaInputMethodLocatorList.size(); i++) {
 456             InputMethodLocator candidate = javaInputMethodLocatorList.get(i);
 457             if (candidate.isLocaleAvailable(locale)) {
 458                 return candidate.deriveLocator(locale);
 459             }
 460         }
 461         return null;
 462     }
 463 
 464     Locale getDefaultKeyboardLocale() {
 465         Toolkit toolkit = Toolkit.getDefaultToolkit();
 466         if (toolkit instanceof InputMethodSupport) {
 467             return ((InputMethodSupport)toolkit).getDefaultKeyboardLocale();
 468         } else {
 469             return Locale.getDefault();
 470         }
 471     }
 472 
 473     /**
 474      * Returns a InputMethodLocator object that the
 475      * user prefers for the given locale.
 476      *
 477      * @param locale Locale for which the user prefers the input method.
 478      */
 479     private synchronized InputMethodLocator getPreferredInputMethod(Locale locale) {
 480         InputMethodLocator preferredLocator = null;
 481 
 482         if (!hasMultipleInputMethods()) {
 483             // No need to look for a preferred Java input method
 484             return null;
 485         }
 486 
 487         // look for the cached preference first.
 488         preferredLocator = preferredLocatorCache.get(locale.toString().intern());
 489         if (preferredLocator != null) {
 490             return preferredLocator;
 491         }
 492 
 493         // look for the preference in the user preference tree
 494         String nodePath = findPreferredInputMethodNode(locale);
 495         String descriptorName = readPreferredInputMethod(nodePath);
 496         Locale advertised;
 497 
 498         // get the locator object
 499         if (descriptorName != null) {
 500             // check for the host adapter first
 501             if (hostAdapterLocator != null &&
 502                 hostAdapterLocator.getDescriptor().getClass().getName().equals(descriptorName)) {
 503                 advertised = getAdvertisedLocale(hostAdapterLocator, locale);
 504                 if (advertised != null) {
 505                     preferredLocator = hostAdapterLocator.deriveLocator(advertised);
 506                     preferredLocatorCache.put(locale.toString().intern(), preferredLocator);
 507                 }
 508                 return preferredLocator;
 509             }
 510             // look for Java input methods
 511             for (int i = 0; i < javaInputMethodLocatorList.size(); i++) {
 512                 InputMethodLocator locator = javaInputMethodLocatorList.get(i);
 513                 InputMethodDescriptor descriptor = locator.getDescriptor();
 514                 if (descriptor.getClass().getName().equals(descriptorName)) {
 515                     advertised = getAdvertisedLocale(locator, locale);
 516                     if (advertised != null) {
 517                         preferredLocator = locator.deriveLocator(advertised);
 518                         preferredLocatorCache.put(locale.toString().intern(), preferredLocator);
 519                     }
 520                     return preferredLocator;
 521                 }
 522             }
 523 
 524             // maybe preferred input method information is bogus.
 525             writePreferredInputMethod(nodePath, null);
 526         }
 527 
 528         return null;
 529     }
 530 
 531     private String findPreferredInputMethodNode(Locale locale) {
 532         if (userRoot == null) {
 533             return null;
 534         }
 535 
 536         // create locale node relative path
 537         String nodePath = preferredIMNode + "/" + createLocalePath(locale);
 538 
 539         // look for the descriptor
 540         while (!nodePath.equals(preferredIMNode)) {
 541             try {
 542                 if (userRoot.nodeExists(nodePath)) {
 543                     if (readPreferredInputMethod(nodePath) != null) {
 544                         return nodePath;
 545                     }
 546                 }
 547             } catch (BackingStoreException bse) {
 548             }
 549 
 550             // search at parent's node
 551             nodePath = nodePath.substring(0, nodePath.lastIndexOf('/'));
 552         }
 553 
 554         return null;
 555     }
 556 
 557     private String readPreferredInputMethod(String nodePath) {
 558         if ((userRoot == null) || (nodePath == null)) {
 559             return null;
 560         }
 561 
 562         return userRoot.node(nodePath).get(descriptorKey, null);
 563     }
 564 
 565     /**
 566      * Writes the preferred input method descriptor class name into
 567      * the user's Preferences tree in accordance with the given locale.
 568      *
 569      * @param locator input method locator to remember.
 570      */
 571     private synchronized void putPreferredInputMethod(InputMethodLocator locator) {
 572         InputMethodDescriptor descriptor = locator.getDescriptor();
 573         Locale preferredLocale = locator.getLocale();
 574 
 575         if (preferredLocale == null) {
 576             // check available locales of the input method
 577             try {
 578                 Locale[] availableLocales = descriptor.getAvailableLocales();
 579                 if (availableLocales.length == 1) {
 580                     preferredLocale = availableLocales[0];
 581                 } else {
 582                     // there is no way to know which locale is the preferred one, so do nothing.
 583                     return;
 584                 }
 585             } catch (AWTException ae) {
 586                 // do nothing here, either.
 587                 return;
 588             }
 589         }
 590 
 591         // for regions that have only one language, we need to regard
 592         // "xx_YY" as "xx" when putting the preference into tree
 593         if (preferredLocale.equals(Locale.JAPAN)) {
 594             preferredLocale = Locale.JAPANESE;
 595         }
 596         if (preferredLocale.equals(Locale.KOREA)) {
 597             preferredLocale = Locale.KOREAN;
 598         }
 599         if (preferredLocale.equals(new Locale("th", "TH"))) {
 600             preferredLocale = new Locale("th");
 601         }
 602 
 603         // obtain node
 604         String path = preferredIMNode + "/" + createLocalePath(preferredLocale);
 605 
 606         // write in the preference tree
 607         writePreferredInputMethod(path, descriptor.getClass().getName());
 608         preferredLocatorCache.put(preferredLocale.toString().intern(),
 609             locator.deriveLocator(preferredLocale));
 610 
 611         return;
 612     }
 613 
 614     private String createLocalePath(Locale locale) {
 615         String language = locale.getLanguage();
 616         String country = locale.getCountry();
 617         String variant = locale.getVariant();
 618         String localePath = null;
 619         if (!variant.equals("")) {
 620             localePath = "_" + language + "/_" + country + "/_" + variant;
 621         } else if (!country.equals("")) {
 622             localePath = "_" + language + "/_" + country;
 623         } else {
 624             localePath = "_" + language;
 625         }
 626 
 627         return localePath;
 628     }
 629 
 630     private void writePreferredInputMethod(String path, String descriptorName) {
 631         if (userRoot != null) {
 632             Preferences node = userRoot.node(path);
 633 
 634             // record it
 635             if (descriptorName != null) {
 636                 node.put(descriptorKey, descriptorName);
 637             } else {
 638                 node.remove(descriptorKey);
 639             }
 640         }
 641     }
 642 
 643     private Preferences getUserRoot() {
 644         return AccessController.doPrivileged(new PrivilegedAction<Preferences>() {
 645             public Preferences run() {
 646                 return Preferences.userRoot();
 647             }
 648         });
 649     }
 650 
 651     private Locale getAdvertisedLocale(InputMethodLocator locator, Locale locale) {
 652         Locale advertised = null;
 653 
 654         if (locator.isLocaleAvailable(locale)) {
 655             advertised = locale;
 656         } else if (locale.getLanguage().equals("ja")) {
 657             // for Japanese, Korean, and Thai, check whether the input method supports
 658             // language or language_COUNTRY.
 659             if (locator.isLocaleAvailable(Locale.JAPAN)) {
 660                 advertised = Locale.JAPAN;
 661             } else if (locator.isLocaleAvailable(Locale.JAPANESE)) {
 662                 advertised = Locale.JAPANESE;
 663             }
 664         } else if (locale.getLanguage().equals("ko")) {
 665             if (locator.isLocaleAvailable(Locale.KOREA)) {
 666                 advertised = Locale.KOREA;
 667             } else if (locator.isLocaleAvailable(Locale.KOREAN)) {
 668                 advertised = Locale.KOREAN;
 669             }
 670         } else if (locale.getLanguage().equals("th")) {
 671             if (locator.isLocaleAvailable(new Locale("th", "TH"))) {
 672                 advertised = new Locale("th", "TH");
 673             } else if (locator.isLocaleAvailable(new Locale("th"))) {
 674                 advertised = new Locale("th");
 675             }
 676         }
 677 
 678         return advertised;
 679     }
 680 }