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.loadInstalled(InputMethodDescriptor.class)) { 263 ClassLoader cl = descriptor.getClass().getClassLoader(); 264 javaInputMethodLocatorList.add(new InputMethodLocator(descriptor, cl, null)); 265 } 266 return null; 267 } 268 }); 269 } catch (PrivilegedActionException e) { 270 e.printStackTrace(); 271 } 272 javaInputMethodCount = javaInputMethodLocatorList.size(); 273 } 274 275 if (hasMultipleInputMethods()) { 276 // initialize preferences 277 if (userRoot == null) { 278 userRoot = getUserRoot(); 279 } 280 } else { 281 // indicate to clients not to offer the menu 282 triggerMenuString = null; 283 } 284 } 285 286 private void showInputMethodMenu() { 287 288 if (!hasMultipleInputMethods()) { 289 requestComponent = null; 290 return; 291 } 292 293 // initialize pop-up menu 294 selectionMenu = InputMethodPopupMenu.getInstance(requestComponent, selectInputMethodMenuTitle); 295 296 // we have to rebuild the menu each time because 297 // some input methods (such as IIIMP) may change 298 // their list of supported locales dynamically 299 selectionMenu.removeAll(); 300 301 // get information about the currently selected input method 302 // ??? if there's no current input context, what's the point 303 // of showing the menu? 304 String currentSelection = getCurrentSelection(); 305 306 // Add menu item for host adapter 307 if (hostAdapterLocator != null) { 308 selectionMenu.addOneInputMethodToMenu(hostAdapterLocator, currentSelection); 309 selectionMenu.addSeparator(); 310 } 311 312 // Add menu items for other input methods 313 for (int i = 0; i < javaInputMethodLocatorList.size(); i++) { 314 InputMethodLocator locator = javaInputMethodLocatorList.get(i); 315 selectionMenu.addOneInputMethodToMenu(locator, currentSelection); 316 } 317 318 synchronized (this) { 319 selectionMenu.addToComponent(requestComponent); 320 requestInputContext = currentInputContext; 321 selectionMenu.show(requestComponent, 60, 80); // TODO: get proper x, y... 322 requestComponent = null; 323 } 324 } 325 326 private String getCurrentSelection() { 327 InputContext inputContext = currentInputContext; 328 if (inputContext != null) { 329 InputMethodLocator locator = inputContext.getInputMethodLocator(); 330 if (locator != null) { 331 return locator.getActionCommandString(); 332 } 333 } 334 return null; 335 } 336 337 synchronized void changeInputMethod(String choice) { 338 InputMethodLocator locator = null; 339 340 String inputMethodName = choice; 341 String localeString = null; 342 int index = choice.indexOf('\n'); 343 if (index != -1) { 344 localeString = choice.substring(index + 1); 345 inputMethodName = choice.substring(0, index); 346 } 347 if (hostAdapterLocator.getActionCommandString().equals(inputMethodName)) { 348 locator = hostAdapterLocator; 349 } else { 350 for (int i = 0; i < javaInputMethodLocatorList.size(); i++) { 351 InputMethodLocator candidate = javaInputMethodLocatorList.get(i); 352 String name = candidate.getActionCommandString(); 353 if (name.equals(inputMethodName)) { 354 locator = candidate; 355 break; 356 } 357 } 358 } 359 360 if (locator != null && localeString != null) { 361 String language = "", country = "", variant = ""; 362 int postIndex = localeString.indexOf('_'); 363 if (postIndex == -1) { 364 language = localeString; 365 } else { 366 language = localeString.substring(0, postIndex); 367 int preIndex = postIndex + 1; 368 postIndex = localeString.indexOf('_', preIndex); 369 if (postIndex == -1) { 370 country = localeString.substring(preIndex); 371 } else { 372 country = localeString.substring(preIndex, postIndex); 373 variant = localeString.substring(postIndex + 1); 374 } 375 } 376 Locale locale = new Locale(language, country, variant); 377 locator = locator.deriveLocator(locale); 378 } 379 380 if (locator == null) 381 return; 382 383 // tell the input context about the change 384 if (requestInputContext != null) { 385 requestInputContext.changeInputMethod(locator); 386 requestInputContext = null; 387 388 // remember the selection 389 putPreferredInputMethod(locator); 390 } 391 } 392 393 InputMethodLocator findInputMethod(Locale locale) { 394 // look for preferred input method first 395 InputMethodLocator locator = getPreferredInputMethod(locale); 396 if (locator != null) { 397 return locator; 398 } 399 400 if (hostAdapterLocator != null && hostAdapterLocator.isLocaleAvailable(locale)) { 401 return hostAdapterLocator.deriveLocator(locale); 402 } 403 404 // Update the locator list 405 initializeInputMethodLocatorList(); 406 407 for (int i = 0; i < javaInputMethodLocatorList.size(); i++) { 408 InputMethodLocator candidate = javaInputMethodLocatorList.get(i); 409 if (candidate.isLocaleAvailable(locale)) { 410 return candidate.deriveLocator(locale); 411 } 412 } 413 return null; 414 } 415 416 Locale getDefaultKeyboardLocale() { 417 Toolkit toolkit = Toolkit.getDefaultToolkit(); 418 if (toolkit instanceof InputMethodSupport) { 419 return ((InputMethodSupport)toolkit).getDefaultKeyboardLocale(); 420 } else { 421 return Locale.getDefault(); 422 } 423 } 424 425 /** 426 * Returns a InputMethodLocator object that the 427 * user prefers for the given locale. 428 * 429 * @param locale Locale for which the user prefers the input method. 430 */ 431 private synchronized InputMethodLocator getPreferredInputMethod(Locale locale) { 432 InputMethodLocator preferredLocator = null; 433 434 if (!hasMultipleInputMethods()) { 435 // No need to look for a preferred Java input method 436 return null; 437 } 438 439 // look for the cached preference first. 440 preferredLocator = preferredLocatorCache.get(locale.toString().intern()); 441 if (preferredLocator != null) { 442 return preferredLocator; 443 } 444 445 // look for the preference in the user preference tree 446 String nodePath = findPreferredInputMethodNode(locale); 447 String descriptorName = readPreferredInputMethod(nodePath); 448 Locale advertised; 449 450 // get the locator object 451 if (descriptorName != null) { 452 // check for the host adapter first 453 if (hostAdapterLocator != null && 454 hostAdapterLocator.getDescriptor().getClass().getName().equals(descriptorName)) { 455 advertised = getAdvertisedLocale(hostAdapterLocator, locale); 456 if (advertised != null) { 457 preferredLocator = hostAdapterLocator.deriveLocator(advertised); 458 preferredLocatorCache.put(locale.toString().intern(), preferredLocator); 459 } 460 return preferredLocator; 461 } 462 // look for Java input methods 463 for (int i = 0; i < javaInputMethodLocatorList.size(); i++) { 464 InputMethodLocator locator = javaInputMethodLocatorList.get(i); 465 InputMethodDescriptor descriptor = locator.getDescriptor(); 466 if (descriptor.getClass().getName().equals(descriptorName)) { 467 advertised = getAdvertisedLocale(locator, locale); 468 if (advertised != null) { 469 preferredLocator = locator.deriveLocator(advertised); 470 preferredLocatorCache.put(locale.toString().intern(), preferredLocator); 471 } 472 return preferredLocator; 473 } 474 } 475 476 // maybe preferred input method information is bogus. 477 writePreferredInputMethod(nodePath, null); 478 } 479 480 return null; 481 } 482 483 private String findPreferredInputMethodNode(Locale locale) { 484 if (userRoot == null) { 485 return null; 486 } 487 488 // create locale node relative path 489 String nodePath = preferredIMNode + "/" + createLocalePath(locale); 490 491 // look for the descriptor 492 while (!nodePath.equals(preferredIMNode)) { 493 try { 494 if (userRoot.nodeExists(nodePath)) { 495 if (readPreferredInputMethod(nodePath) != null) { 496 return nodePath; 497 } 498 } 499 } catch (BackingStoreException bse) { 500 } 501 502 // search at parent's node 503 nodePath = nodePath.substring(0, nodePath.lastIndexOf('/')); 504 } 505 506 return null; 507 } 508 509 private String readPreferredInputMethod(String nodePath) { 510 if ((userRoot == null) || (nodePath == null)) { 511 return null; 512 } 513 514 return userRoot.node(nodePath).get(descriptorKey, null); 515 } 516 517 /** 518 * Writes the preferred input method descriptor class name into 519 * the user's Preferences tree in accordance with the given locale. 520 * 521 * @param inputMethodLocator input method locator to remember. 522 */ 523 private synchronized void putPreferredInputMethod(InputMethodLocator locator) { 524 InputMethodDescriptor descriptor = locator.getDescriptor(); 525 Locale preferredLocale = locator.getLocale(); 526 527 if (preferredLocale == null) { 528 // check available locales of the input method 529 try { 530 Locale[] availableLocales = descriptor.getAvailableLocales(); 531 if (availableLocales.length == 1) { 532 preferredLocale = availableLocales[0]; 533 } else { 534 // there is no way to know which locale is the preferred one, so do nothing. 535 return; 536 } 537 } catch (AWTException ae) { 538 // do nothing here, either. 539 return; 540 } 541 } 542 543 // for regions that have only one language, we need to regard 544 // "xx_YY" as "xx" when putting the preference into tree 545 if (preferredLocale.equals(Locale.JAPAN)) { 546 preferredLocale = Locale.JAPANESE; 547 } 548 if (preferredLocale.equals(Locale.KOREA)) { 549 preferredLocale = Locale.KOREAN; 550 } 551 if (preferredLocale.equals(new Locale("th", "TH"))) { 552 preferredLocale = new Locale("th"); 553 } 554 555 // obtain node 556 String path = preferredIMNode + "/" + createLocalePath(preferredLocale); 557 558 // write in the preference tree 559 writePreferredInputMethod(path, descriptor.getClass().getName()); 560 preferredLocatorCache.put(preferredLocale.toString().intern(), 561 locator.deriveLocator(preferredLocale)); 562 563 return; 564 } 565 566 private String createLocalePath(Locale locale) { 567 String language = locale.getLanguage(); 568 String country = locale.getCountry(); 569 String variant = locale.getVariant(); 570 String localePath = null; 571 if (!variant.equals("")) { 572 localePath = "_" + language + "/_" + country + "/_" + variant; 573 } else if (!country.equals("")) { 574 localePath = "_" + language + "/_" + country; 575 } else { 576 localePath = "_" + language; 577 } 578 579 return localePath; 580 } 581 582 private void writePreferredInputMethod(String path, String descriptorName) { 583 if (userRoot != null) { 584 Preferences node = userRoot.node(path); 585 586 // record it 587 if (descriptorName != null) { 588 node.put(descriptorKey, descriptorName); 589 } else { 590 node.remove(descriptorKey); 591 } 592 } 593 } 594 595 private Preferences getUserRoot() { 596 return AccessController.doPrivileged(new PrivilegedAction<Preferences>() { 597 public Preferences run() { 598 return Preferences.userRoot(); 599 } 600 }); 601 } 602 603 private Locale getAdvertisedLocale(InputMethodLocator locator, Locale locale) { 604 Locale advertised = null; 605 606 if (locator.isLocaleAvailable(locale)) { 607 advertised = locale; 608 } else if (locale.getLanguage().equals("ja")) { 609 // for Japanese, Korean, and Thai, check whether the input method supports 610 // language or language_COUNTRY. 611 if (locator.isLocaleAvailable(Locale.JAPAN)) { 612 advertised = Locale.JAPAN; 613 } else if (locator.isLocaleAvailable(Locale.JAPANESE)) { 614 advertised = Locale.JAPANESE; 615 } 616 } else if (locale.getLanguage().equals("ko")) { 617 if (locator.isLocaleAvailable(Locale.KOREA)) { 618 advertised = Locale.KOREA; 619 } else if (locator.isLocaleAvailable(Locale.KOREAN)) { 620 advertised = Locale.KOREAN; 621 } 622 } else if (locale.getLanguage().equals("th")) { 623 if (locator.isLocaleAvailable(new Locale("th", "TH"))) { 624 advertised = new Locale("th", "TH"); 625 } else if (locator.isLocaleAvailable(new Locale("th"))) { 626 advertised = new Locale("th"); 627 } 628 } 629 630 return advertised; 631 } 632 }