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