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 }