1 // © 2016 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html#License 3 /* 4 ******************************************************************************* 5 * Copyright (C) 2004-2011, International Business Machines Corporation and * 6 * others. All Rights Reserved. * 7 ******************************************************************************* 8 */ 9 10 package com.ibm.icu.dev.tool.ime.translit; 11 12 import java.awt.AWTEvent; 13 import java.awt.Color; 14 import java.awt.Component; 15 import java.awt.Dimension; 16 import java.awt.Point; 17 import java.awt.Rectangle; 18 import java.awt.Toolkit; 19 import java.awt.Window; 20 import java.awt.event.ActionEvent; 21 import java.awt.event.ActionListener; 22 import java.awt.event.InputEvent; 23 import java.awt.event.InputMethodEvent; 24 import java.awt.event.KeyEvent; 25 import java.awt.event.MouseEvent; 26 import java.awt.font.TextAttribute; 27 import java.awt.font.TextHitInfo; 28 import java.awt.im.InputMethodHighlight; 29 import java.awt.im.spi.InputMethod; 30 import java.awt.im.spi.InputMethodContext; 31 import java.text.AttributedString; 32 import java.text.Collator; 33 import java.util.Comparator; 34 import java.util.Enumeration; 35 import java.util.Locale; 36 import java.util.MissingResourceException; 37 import java.util.ResourceBundle; 38 import java.util.TreeSet; 39 40 import javax.swing.JComboBox; 41 import javax.swing.JLabel; 42 import javax.swing.JList; 43 import javax.swing.ListCellRenderer; 44 45 import com.ibm.icu.impl.Utility; 46 import com.ibm.icu.lang.UCharacter; 47 import com.ibm.icu.text.ReplaceableString; 48 import com.ibm.icu.text.Transliterator; 49 50 public class TransliteratorInputMethod implements InputMethod { 51 usesAttachedIME()52 private static boolean usesAttachedIME() { 53 // we're in the ext directory so permissions are not an issue 54 String os = System.getProperty("os.name"); 55 if (os != null) { 56 return os.indexOf("Windows") == -1; 57 } 58 return false; 59 } 60 61 // true if Solaris style; false if PC style, assume Apple uses PC style for now 62 private static final boolean attachedStatusWindow = usesAttachedIME(); 63 64 // the shared status window 65 private static Window statusWindow; 66 67 // current or last owner 68 private static TransliteratorInputMethod statusWindowOwner; 69 70 // cache location limits for attached 71 private static Rectangle attachedLimits; 72 73 // convenience of access, to reflect the current state 74 private static JComboBox choices; 75 76 // 77 // per-instance state 78 // 79 80 // if we're attached, the status window follows the client window 81 private Point attachedLocation; 82 83 private static int gid; 84 85 private int id = gid++; 86 87 InputMethodContext imc; 88 private boolean enabled = true; 89 90 private int selectedIndex = -1; // index in JComboBox corresponding to our transliterator 91 private Transliterator transliterator; 92 private int desiredContext; 93 private StringBuffer buffer; 94 private ReplaceableString replaceableText; 95 private Transliterator.Position index; 96 97 // debugging 98 private static boolean TRACE_EVENT = false; 99 private static boolean TRACE_MESSAGES = false; 100 private static boolean TRACE_BUFFER = false; 101 TransliteratorInputMethod()102 public TransliteratorInputMethod() { 103 if (TRACE_MESSAGES) 104 dumpStatus("<constructor>"); 105 106 buffer = new StringBuffer(); 107 replaceableText = new ReplaceableString(buffer); 108 index = new Transliterator.Position(); 109 } 110 dumpStatus(String msg)111 public void dumpStatus(String msg) { 112 System.out.println("(" + this + ") " + msg); 113 } 114 setInputMethodContext(InputMethodContext context)115 public void setInputMethodContext(InputMethodContext context) { 116 initStatusWindow(context); 117 118 imc = context; 119 imc.enableClientWindowNotification(this, attachedStatusWindow); 120 } 121 initStatusWindow(InputMethodContext context)122 private static void initStatusWindow(InputMethodContext context) { 123 if (statusWindow == null) { 124 String title; 125 try { 126 ResourceBundle rb = ResourceBundle 127 .getBundle("com.ibm.icu.dev.tool.ime.translit.Transliterator"); 128 title = rb.getString("title"); 129 } catch (MissingResourceException m) { 130 System.out.println("Transliterator resources missing: " + m); 131 title = "Transliterator Input Method"; 132 } 133 134 Window sw = context.createInputMethodWindow(title, false); 135 136 // get all the ICU Transliterators 137 Enumeration en = Transliterator.getAvailableIDs(); 138 TreeSet types = new TreeSet(new LabelComparator()); 139 140 while (en.hasMoreElements()) { 141 String id = (String) en.nextElement(); 142 String name = Transliterator.getDisplayName(id); 143 JLabel label = new JLabel(name); 144 label.setName(id); 145 types.add(label); 146 } 147 148 // add the transliterators to the combo box 149 150 choices = new JComboBox(types.toArray()); 151 152 choices.setEditable(false); 153 choices.setSelectedIndex(0); 154 choices.setRenderer(new NameRenderer()); 155 choices.setActionCommand("transliterator"); 156 157 choices.addActionListener(new ActionListener() { 158 public void actionPerformed(ActionEvent e) { 159 if (statusWindowOwner != null) { 160 statusWindowOwner.statusWindowAction(e); 161 } 162 } 163 }); 164 165 sw.add(choices); 166 sw.pack(); 167 168 Dimension sd = Toolkit.getDefaultToolkit().getScreenSize(); 169 Dimension wd = sw.getSize(); 170 if (attachedStatusWindow) { 171 attachedLimits = new Rectangle(0, 0, sd.width - wd.width, 172 sd.height - wd.height); 173 } else { 174 sw.setLocation(sd.width - wd.width, sd.height - wd.height - 25); 175 } 176 177 synchronized (TransliteratorInputMethod.class) { 178 if (statusWindow == null) { 179 statusWindow = sw; 180 } 181 } 182 } 183 } 184 statusWindowAction(ActionEvent e)185 private void statusWindowAction(ActionEvent e) { 186 if (TRACE_MESSAGES) 187 dumpStatus(">>status window action"); 188 JComboBox cb = (JComboBox) e.getSource(); 189 int si = cb.getSelectedIndex(); 190 if (si != selectedIndex) { // otherwise, we don't need to change 191 if (TRACE_MESSAGES) 192 dumpStatus("status window action oldIndex: " + selectedIndex 193 + " newIndex: " + si); 194 195 selectedIndex = si; 196 197 JLabel item = (JLabel) cb.getSelectedItem(); 198 199 // construct the actual transliterator 200 // commit any text that may be present first 201 commitAll(); 202 203 transliterator = Transliterator.getInstance(item.getName()); 204 desiredContext = transliterator.getMaximumContextLength(); 205 206 reset(); 207 } 208 if (TRACE_MESSAGES) 209 dumpStatus("<<status window action"); 210 } 211 212 // java has no pin to rectangle function? pin(Point p, Rectangle r)213 private static void pin(Point p, Rectangle r) { 214 if (p.x < r.x) { 215 p.x = r.x; 216 } else if (p.x > r.x + r.width) { 217 p.x = r.x + r.width; 218 } 219 if (p.y < r.y) { 220 p.y = r.y; 221 } else if (p.y > r.y + r.height) { 222 p.y = r.y + r.height; 223 } 224 } 225 notifyClientWindowChange(Rectangle location)226 public void notifyClientWindowChange(Rectangle location) { 227 if (TRACE_MESSAGES) 228 dumpStatus(">>notify client window change: " + location); 229 synchronized (TransliteratorInputMethod.class) { 230 if (statusWindowOwner == this) { 231 if (location == null) { 232 statusWindow.setVisible(false); 233 } else { 234 attachedLocation = new Point(location.x, location.y 235 + location.height); 236 pin(attachedLocation, attachedLimits); 237 statusWindow.setLocation(attachedLocation); 238 statusWindow.setVisible(true); 239 } 240 } 241 } 242 if (TRACE_MESSAGES) 243 dumpStatus("<<notify client window change: " + location); 244 } 245 activate()246 public void activate() { 247 if (TRACE_MESSAGES) 248 dumpStatus(">>activate"); 249 250 synchronized (TransliteratorInputMethod.class) { 251 if (statusWindowOwner != this) { 252 if (TRACE_MESSAGES) 253 dumpStatus("setStatusWindowOwner from: " + statusWindowOwner + " to: " + this); 254 255 statusWindowOwner = this; 256 // will be null before first change notification 257 if (attachedStatusWindow && attachedLocation != null) { 258 statusWindow.setLocation(attachedLocation); 259 } 260 choices.setSelectedIndex(selectedIndex == -1 ? choices 261 .getSelectedIndex() : selectedIndex); 262 } 263 264 choices.setForeground(Color.BLACK); 265 statusWindow.setVisible(true); 266 } 267 if (TRACE_MESSAGES) 268 dumpStatus("<<activate"); 269 } 270 deactivate(boolean isTemporary)271 public void deactivate(boolean isTemporary) { 272 if (TRACE_MESSAGES) 273 dumpStatus(">>deactivate" + (isTemporary ? " (temporary)" : "")); 274 if (!isTemporary) { 275 synchronized (TransliteratorInputMethod.class) { 276 choices.setForeground(Color.LIGHT_GRAY); 277 } 278 } 279 if (TRACE_MESSAGES) 280 dumpStatus("<<deactivate" + (isTemporary ? " (temporary)" : "")); 281 } 282 hideWindows()283 public void hideWindows() { 284 if (TRACE_MESSAGES) 285 dumpStatus(">>hideWindows"); 286 synchronized (TransliteratorInputMethod.class) { 287 if (statusWindowOwner == this) { 288 if (TRACE_MESSAGES) 289 dumpStatus("hiding"); 290 statusWindow.setVisible(false); 291 } 292 } 293 if (TRACE_MESSAGES) 294 dumpStatus("<<hideWindows"); 295 } 296 setLocale(Locale locale)297 public boolean setLocale(Locale locale) { 298 return false; 299 } 300 getLocale()301 public Locale getLocale() { 302 return Locale.getDefault(); 303 } 304 setCharacterSubsets(Character.Subset[] subsets)305 public void setCharacterSubsets(Character.Subset[] subsets) { 306 } 307 reconvert()308 public void reconvert() { 309 throw new UnsupportedOperationException(); 310 } 311 removeNotify()312 public void removeNotify() { 313 if (TRACE_MESSAGES) 314 dumpStatus("**removeNotify"); 315 } 316 endComposition()317 public void endComposition() { 318 commitAll(); 319 } 320 dispose()321 public void dispose() { 322 if (TRACE_MESSAGES) 323 dumpStatus("**dispose"); 324 } 325 getControlObject()326 public Object getControlObject() { 327 return null; 328 } 329 setCompositionEnabled(boolean enable)330 public void setCompositionEnabled(boolean enable) { 331 enabled = enable; 332 } 333 isCompositionEnabled()334 public boolean isCompositionEnabled() { 335 return enabled; 336 } 337 338 // debugging eventInfo(AWTEvent event)339 private String eventInfo(AWTEvent event) { 340 String info = event.toString(); 341 StringBuffer buf = new StringBuffer(); 342 int index1 = info.indexOf("["); 343 int index2 = info.indexOf(",", index1); 344 buf.append(info.substring(index1 + 1, index2)); 345 346 index1 = info.indexOf("] on "); 347 index2 = info.indexOf("[", index1); 348 if (index2 != -1) { 349 int index3 = info.lastIndexOf(".", index2); 350 if (index3 < index1 + 4) { 351 index3 = index1 + 4; 352 } 353 buf.append(" on "); 354 buf.append(info.substring(index3 + 1, index2)); 355 } 356 return buf.toString(); 357 } 358 dispatchEvent(AWTEvent event)359 public void dispatchEvent(AWTEvent event) { 360 final int MODIFIERS = 361 InputEvent.CTRL_MASK | 362 InputEvent.META_MASK | 363 InputEvent.ALT_MASK | 364 InputEvent.ALT_GRAPH_MASK; 365 366 switch (event.getID()) { 367 case MouseEvent.MOUSE_PRESSED: 368 if (enabled) { 369 if (TRACE_EVENT) System.out.println("TIM: " + eventInfo(event)); 370 // we'll get this even if the user is scrolling, can we rely on the component? 371 // commitAll(); // don't allow even clicks within our own edit area 372 } 373 break; 374 375 case KeyEvent.KEY_TYPED: { 376 if (enabled) { 377 KeyEvent ke = (KeyEvent)event; 378 if (TRACE_EVENT) System.out.println("TIM: " + eventInfo(ke)); 379 if ((ke.getModifiers() & MODIFIERS) != 0) { 380 commitAll(); // assume a command, let it go through 381 } else { 382 if (handleTyped(ke.getKeyChar())) { 383 ke.consume(); 384 } 385 } 386 } 387 } break; 388 389 case KeyEvent.KEY_PRESSED: { 390 if (enabled) { 391 KeyEvent ke = (KeyEvent)event; 392 if (TRACE_EVENT) System.out.println("TIM: " + eventInfo(ke)); 393 if (handlePressed(ke.getKeyCode())) { 394 ke.consume(); 395 } 396 } 397 } break; 398 399 case KeyEvent.KEY_RELEASED: { 400 // this won't autorepeat, which is better for toggle actions 401 KeyEvent ke = (KeyEvent)event; 402 if (ke.getKeyCode() == KeyEvent.VK_SPACE && ke.isControlDown()) { 403 setCompositionEnabled(!enabled); 404 } 405 } break; 406 407 default: 408 break; 409 } 410 } 411 412 /** Wipe clean */ reset()413 private void reset() { 414 buffer.delete(0, buffer.length()); 415 index.contextStart = index.contextLimit = index.start = index.limit = 0; 416 } 417 418 // committed}context-composed|composed 419 // ^ ^ ^ 420 // cc start ctxLim 421 traceBuffer(String msg, int cc, int off)422 private void traceBuffer(String msg, int cc, int off) { 423 if (TRACE_BUFFER) 424 System.out.println(Utility.escape(msg + ": '" 425 + buffer.substring(0, cc) + '}' 426 + buffer.substring(cc, index.start) + '-' 427 + buffer.substring(index.start, index.contextLimit) + '|' 428 + buffer.substring(index.contextLimit) + '\'')); 429 } 430 update(boolean flush)431 private void update(boolean flush) { 432 int len = buffer.length(); 433 String text = buffer.toString(); 434 AttributedString as = new AttributedString(text); 435 436 int cc, off; 437 if (flush) { 438 off = index.contextLimit - len; // will be negative 439 cc = index.start = index.limit = index.contextLimit = len; 440 } else { 441 cc = index.start > desiredContext ? index.start - desiredContext 442 : 0; 443 off = index.contextLimit - cc; 444 } 445 446 if (index.start < len) { 447 as.addAttribute(TextAttribute.INPUT_METHOD_HIGHLIGHT, 448 InputMethodHighlight.SELECTED_RAW_TEXT_HIGHLIGHT, 449 index.start, len); 450 } 451 452 imc.dispatchInputMethodEvent( 453 InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, as.getIterator(), 454 cc, TextHitInfo.leading(off), null); 455 456 traceBuffer("update", cc, off); 457 458 if (cc > 0) { 459 buffer.delete(0, cc); 460 index.start -= cc; 461 index.limit -= cc; 462 index.contextLimit -= cc; 463 } 464 } 465 updateCaret()466 private void updateCaret() { 467 imc.dispatchInputMethodEvent(InputMethodEvent.CARET_POSITION_CHANGED, 468 null, 0, TextHitInfo.leading(index.contextLimit), null); 469 traceBuffer("updateCaret", 0, index.contextLimit); 470 } 471 caretToStart()472 private void caretToStart() { 473 if (index.contextLimit > index.start) { 474 index.contextLimit = index.limit = index.start; 475 updateCaret(); 476 } 477 } 478 caretToLimit()479 private void caretToLimit() { 480 if (index.contextLimit < buffer.length()) { 481 index.contextLimit = index.limit = buffer.length(); 482 updateCaret(); 483 } 484 } 485 caretTowardsStart()486 private boolean caretTowardsStart() { 487 int bufpos = index.contextLimit; 488 if (bufpos > index.start) { 489 --bufpos; 490 if (bufpos > index.start 491 && UCharacter.isLowSurrogate(buffer.charAt(bufpos)) 492 && UCharacter.isHighSurrogate(buffer.charAt(bufpos - 1))) { 493 --bufpos; 494 } 495 index.contextLimit = index.limit = bufpos; 496 updateCaret(); 497 return true; 498 } 499 return commitAll(); 500 } 501 caretTowardsLimit()502 private boolean caretTowardsLimit() { 503 int bufpos = index.contextLimit; 504 if (bufpos < buffer.length()) { 505 ++bufpos; 506 if (bufpos < buffer.length() 507 && UCharacter.isLowSurrogate(buffer.charAt(bufpos)) 508 && UCharacter.isHighSurrogate(buffer.charAt(bufpos - 1))) { 509 ++bufpos; 510 } 511 index.contextLimit = index.limit = bufpos; 512 updateCaret(); 513 return true; 514 } 515 return commitAll(); 516 } 517 canBackspace()518 private boolean canBackspace() { 519 return index.contextLimit > 0; 520 } 521 backspace()522 private boolean backspace() { 523 int bufpos = index.contextLimit; 524 if (bufpos > 0) { 525 int limit = bufpos; 526 --bufpos; 527 if (bufpos > 0 && UCharacter.isLowSurrogate(buffer.charAt(bufpos)) 528 && UCharacter.isHighSurrogate(buffer.charAt(bufpos - 1))) { 529 --bufpos; 530 } 531 if (bufpos < index.start) { 532 index.start = bufpos; 533 } 534 index.contextLimit = index.limit = bufpos; 535 doDelete(bufpos, limit); 536 return true; 537 } 538 return false; 539 } 540 canDelete()541 private boolean canDelete() { 542 return index.contextLimit < buffer.length(); 543 } 544 delete()545 private boolean delete() { 546 int bufpos = index.contextLimit; 547 if (bufpos < buffer.length()) { 548 int limit = bufpos + 1; 549 if (limit < buffer.length() 550 && UCharacter.isHighSurrogate(buffer.charAt(limit - 1)) 551 && UCharacter.isLowSurrogate(buffer.charAt(limit))) { 552 ++limit; 553 } 554 doDelete(bufpos, limit); 555 return true; 556 } 557 return false; 558 } 559 doDelete(int start, int limit)560 private void doDelete(int start, int limit) { 561 buffer.delete(start, limit); 562 update(false); 563 } 564 commitAll()565 private boolean commitAll() { 566 if (buffer.length() > 0) { 567 boolean atStart = index.start == index.contextLimit; 568 boolean didConvert = buffer.length() > index.start; 569 index.contextLimit = index.limit = buffer.length(); 570 transliterator.finishTransliteration(replaceableText, index); 571 if (atStart) { 572 index.start = index.limit = index.contextLimit = 0; 573 } 574 update(true); 575 return didConvert; 576 } 577 return false; 578 } 579 clearAll()580 private void clearAll() { 581 int len = buffer.length(); 582 if (len > 0) { 583 if (len > index.start) { 584 buffer.delete(index.start, len); 585 } 586 update(true); 587 } 588 } 589 insert(char c)590 private boolean insert(char c) { 591 transliterator.transliterate(replaceableText, index, c); 592 update(false); 593 return true; 594 } 595 editing()596 private boolean editing() { 597 return buffer.length() > 0; 598 } 599 600 /** 601 * The big problem is that from release to release swing changes how it 602 * handles some characters like tab and backspace. Sometimes it handles 603 * them as keyTyped events, and sometimes it handles them as keyPressed 604 * events. If you want to allow the event to go through so swing handles 605 * it, you have to allow one or the other to go through. If you don't want 606 * the event to go through so you can handle it, you have to stop the 607 * event both places. 608 * @return whether the character was handled 609 */ handleTyped(char ch)610 private boolean handleTyped(char ch) { 611 if (enabled) { 612 switch (ch) { 613 case '\b': if (editing()) return backspace(); break; 614 case '\t': if (editing()) { return commitAll(); } break; 615 case '\u001b': if (editing()) { clearAll(); return true; } break; 616 case '\u007f': if (editing()) return delete(); break; 617 default: return insert(ch); 618 } 619 } 620 return false; 621 } 622 623 /** 624 * Handle keyPressed events. 625 */ handlePressed(int code)626 private boolean handlePressed(int code) { 627 if (enabled && editing()) { 628 switch (code) { 629 case KeyEvent.VK_PAGE_UP: 630 case KeyEvent.VK_UP: 631 case KeyEvent.VK_KP_UP: 632 case KeyEvent.VK_HOME: 633 caretToStart(); return true; 634 case KeyEvent.VK_PAGE_DOWN: 635 case KeyEvent.VK_DOWN: 636 case KeyEvent.VK_KP_DOWN: 637 case KeyEvent.VK_END: 638 caretToLimit(); return true; 639 case KeyEvent.VK_LEFT: 640 case KeyEvent.VK_KP_LEFT: 641 return caretTowardsStart(); 642 case KeyEvent.VK_RIGHT: 643 case KeyEvent.VK_KP_RIGHT: 644 return caretTowardsLimit(); 645 case KeyEvent.VK_BACK_SPACE: 646 return canBackspace(); // unfortunately, in 1.5 swing handles this in keyPressed instead of keyTyped 647 case KeyEvent.VK_DELETE: 648 return canDelete(); // this too? 649 case KeyEvent.VK_TAB: 650 case KeyEvent.VK_ENTER: 651 return commitAll(); // so we'll never handle VK_TAB in keyTyped 652 653 case KeyEvent.VK_SHIFT: 654 case KeyEvent.VK_CONTROL: 655 case KeyEvent.VK_ALT: 656 return false; // ignore these unless a key typed event gets generated 657 default: 658 // by default, let editor handle it, and we'll assume that it will tell us 659 // to endComposition if it does anything funky with, e.g., function keys. 660 return false; 661 } 662 } 663 return false; 664 } 665 toString()666 public String toString() { 667 final String[] names = { 668 "alice", "bill", "carrie", "doug", "elena", "frank", "gertie", "howie", "ingrid", "john" 669 }; 670 671 if (id < names.length) { 672 return names[id]; 673 } else { 674 return names[id] + "-" + (id/names.length); 675 } 676 } 677 } 678 679 class NameRenderer extends JLabel implements ListCellRenderer { 680 681 /** 682 * For serialization 683 */ 684 private static final long serialVersionUID = -210152863798631747L; 685 getListCellRendererComponent( JList list, Object value, int index, boolean isSelected, boolean cellHasFocus)686 public Component getListCellRendererComponent( 687 JList list, 688 Object value, 689 int index, 690 boolean isSelected, 691 boolean cellHasFocus) { 692 693 String s = ((JLabel)value).getText(); 694 setText(s); 695 696 if (isSelected) { 697 setBackground(list.getSelectionBackground()); 698 setForeground(list.getSelectionForeground()); 699 } else { 700 setBackground(list.getBackground()); 701 setForeground(list.getForeground()); 702 } 703 704 setEnabled(list.isEnabled()); 705 setFont(list.getFont()); 706 setOpaque(true); 707 return this; 708 } 709 } 710 711 class LabelComparator implements Comparator { compare(Object obj1, Object obj2)712 public int compare(Object obj1, Object obj2) { 713 Collator collator = Collator.getInstance(); 714 return collator.compare(((JLabel)obj1).getText(), ((JLabel)obj2).getText()); 715 } 716 equals(Object obj1)717 public boolean equals(Object obj1) { 718 return obj1 instanceof LabelComparator; 719 } 720 } 721