1   /*
2    * GateIM.java
3    *
4    * Copyright (c) 1998-2005, The University of Sheffield.
5    *
6    * This file is part of GATE (see http://gate.ac.uk/), and is free
7    * software, licenced under the GNU Library General Public License,
8    * Version 2, June1991.
9    *
10   * A copy of this licence is included in the distribution in the file
11   * licence.html, and is also available at http://gate.ac.uk/gate/licence.html.
12   *
13   * Valentin Tablan, October 2000
14   *
15   * $Id: GateIM.java,v 1.9 2005/01/11 13:51:38 ian Exp $
16   */
17  package guk.im;
18  
19  import java.awt.*;
20  import java.awt.event.InputMethodEvent;
21  import java.awt.event.KeyEvent;
22  import java.awt.im.spi.InputMethod;
23  import java.awt.im.spi.InputMethodContext;
24  import java.io.IOException;
25  import java.lang.Character.Subset;
26  import java.text.AttributedString;
27  import java.util.*;
28  
29  /**
30   * The Gate input method
31   *
32   */
33  public class GateIM implements InputMethod {
34  
35    /**
36     * Constructs a new Gate input method
37     *
38     * @param supportedLocales
39     */
40    public GateIM(Map supportedLocales) {
41      this.supportedLocales = supportedLocales;
42      loadedLocales = new HashMap();
43    }// GateIM(Map supportedLocales)
44  
45    /**
46     * Provides the input method with a context. This method is called by the
47     * system after the input method is loaded and linked to a text component.
48     *
49     * @param context
50     */
51    public void setInputMethodContext(InputMethodContext context) {
52      myContext = context;
53      //we don't care about the client window state and position
54      myContext.enableClientWindowNotification(this, false);
55    }// setInputMethodContext(InputMethodContext context)
56  
57    /**
58     * Selects the active locale
59     *
60     * @param locale
61     */
62    public boolean setLocale(Locale locale) {
63      endComposition();
64      try {
65        if(supportedLocales.containsKey(locale)){
66          currentLocale = locale;
67          loadLocale(locale);
68          if(keyboardMap != null) keyboardMap.update(currentHandler,
69                                                     currentState);
70          return true;
71        }
72      } catch(IllegalArgumentException iae){
73        iae.printStackTrace();
74        return false;
75      }
76      return false;
77    }// boolean setLocale(Locale locale)
78  
79    /**
80     * Gets the active locale
81     *
82     */
83    public Locale getLocale() {
84      return currentLocale;
85    }
86  
87    /**
88     * gets the descriptor class for this input method
89     *
90     */
91    public GateIMDescriptor getDescriptor(){
92      return new GateIMDescriptor();
93    }
94  
95    /**
96     * Restricts the character ranges valid for this input method output. This is
97     * currently ignored by the input method.
98     *
99     * @param subsets
100    */
101   public void setCharacterSubsets(Subset[] subsets) {
102   }
103 
104   /**
105    * Enables this input method for composition
106    *
107    * @param enable
108    */
109   public void setCompositionEnabled(boolean enable) {
110     enabled = enable;
111   }
112 
113   /**
114    * Is this input method enabled?
115    *
116    */
117   public boolean isCompositionEnabled() {
118     return enabled;
119   }
120 
121   /**
122    * Throws a UnsupportedOperationException as this input method does not
123    * support recnversion.
124    *
125    */
126   public void reconvert() {
127     /**@todo: Implement this java.awt.im.spi.InputMethod method*/
128     throw new java.lang.UnsupportedOperationException(
129                         "Reconversion not supported!");
130   }
131 
132   /**
133    * Called by the system when an input event occures in a component that uses
134    * this input method.
135    * The input method then analyses the input event and sends an input method
136    * event to the interested components
137    * using the input context provided by the system.
138    *
139    * @param event
140    */
141   public void dispatchEvent(AWTEvent event) {
142     if(event instanceof KeyEvent){
143       KeyEvent kEvent = (KeyEvent) event;
144       char ch = kEvent.getKeyChar();
145       int keyCode = kEvent.getKeyCode();
146       int modifiers = kEvent.getModifiers();
147       int id = kEvent.getID();
148       //process the CTRL+? events that do not generate key-typed events.
149       if((id == KeyEvent.KEY_PRESSED || id == KeyEvent.KEY_RELEASED) &&
150          (modifiers & KeyEvent.CTRL_MASK) > 0 &&
151          keyCode != KeyEvent.VK_CONTROL){
152         boolean shift = (modifiers & KeyEvent.SHIFT_MASK) > 0;
153         if(ch == KeyEvent.CHAR_UNDEFINED ||
154            Character.isISOControl(ch)){
155           if((int)'0' <= keyCode && keyCode <= (int)'9'){
156             if(!shift){
157               ch = (char)keyCode;
158             }else{
159               //shifted versions for the digit keys
160               switch((char)keyCode){
161                 case '0':{
162                   ch = ')';
163                   break;
164                 }
165                 case '1':{
166                   ch = '!';
167                   break;
168                 }
169                 case '2':{
170                   ch = '\"';
171                   break;
172                 }
173                 case '3':{
174                   ch = '\u00a3'; //pound symbol
175                   break;
176                 }
177                 case '4':{
178                   ch = '$';
179                   break;
180                 }
181                 case '5':{
182                   ch = '%';
183                   break;
184                 }
185                 case '6':{
186                   ch = '^';
187                   break;
188                 }
189                 case '7':{
190                   ch = '&';
191                   break;
192                 }
193                 case '8':{
194                   ch = '*';
195                   break;
196                 }
197                 case '9':{
198                   ch = '(';
199                   break;
200                 }
201               }//switch((char)keyCode)
202             }
203           } else if((int)'A' <= keyCode && keyCode <= (int)'Z'){
204             ch = (char)keyCode;
205             if(!shift){
206               ch = Character.toLowerCase(ch);
207             }
208           } else {
209             switch(keyCode){
210               case KeyEvent.VK_MINUS:{
211                 ch = shift?'_':'-';
212                 break;
213               }
214               case KeyEvent.VK_EQUALS:{
215                 ch = shift?'+':'=';
216                 break;
217               }
218               case KeyEvent.VK_OPEN_BRACKET:{
219                 ch = shift?'{':'[';
220                 break;
221               }
222               case KeyEvent.VK_CLOSE_BRACKET:{
223                 ch = shift?'}':']';
224                 break;
225               }
226               case KeyEvent.VK_SEMICOLON:{
227                 ch = shift?':':';';
228                 break;
229               }
230               case KeyEvent.VK_BACK_QUOTE:{
231                 ch = shift?'@':'\'';
232                 break;
233               }
234               case KeyEvent.VK_QUOTE:{
235                 ch = shift?'~':'#';
236                 break;
237               }
238               case KeyEvent.VK_BACK_SLASH:{
239                 ch = shift?'|':'\\';
240                 break;
241               }
242               case KeyEvent.VK_COMMA:{
243                 ch = shift?'<':',';
244                 break;
245               }
246               case KeyEvent.VK_STOP:{
247                 ch = shift?'>':'.';
248                 break;
249               }
250               case KeyEvent.VK_SLASH:{
251                 ch = shift?'?':'/';
252                 break;
253               }
254             }
255           }
256         }//if(ch = KeyEvent.CHAR_UNDEFINED)
257         //modify the event
258         if(id == KeyEvent.KEY_PRESSED) id = KeyEvent.KEY_TYPED;
259       }
260 
261       //now send it to the virtual keyboard
262       ((KeyEvent)event).setKeyChar(ch);
263       if(keyboardMap != null) keyboardMap.addJob(event);
264 
265       //now process it for input
266       if( id == KeyEvent.KEY_TYPED &&
267           ( ch == ' ' ||
268             !Character.isISOControl(ch)
269           )
270         )
271         {
272         //it's a key typed event
273         Key key = new Key(ch, modifiers);
274         Action action = currentState.getNext(key);
275         if(action == null){
276           //we can't go further, commit if in final state cancel otherwise
277           if(currentState.isFinal()){
278             myContext.dispatchInputMethodEvent(
279                   InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
280                   (new AttributedString(composedText)).getIterator(),
281                   composedText.length(), null, null);
282           }
283           //move to the initial state
284           composedText = "";
285           currentState = currentHandler.getInitialState();
286           action = currentState.getNext(key);
287         }
288         if(action ==null){
289           //undefined state, remain in initial state, cancel composed text
290           //send the key char
291           composedText = "";
292         } else {
293           //advance and compose new text
294           currentState = action.getNext();
295           composedText = action.getComposedText();
296         }
297         ((KeyEvent)event).consume();
298         //fire the event to the client
299         boolean commit = !currentState.hasNext();
300         myContext.dispatchInputMethodEvent(
301           InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
302           (new AttributedString(composedText)).getIterator(),
303           commit?composedText.length():0, null,null);
304         if(commit) composedText = "";
305         if(keyboardMap != null) keyboardMap.update(currentHandler, currentState);
306       }
307     }// if
308   }// dispatchEvent(AWTEvent event)
309 
310   /**
311    * Called by the system when the client window has changed size or position.
312    * This event is ignored by the input method.
313    *
314    * @param bounds
315    */
316   public void notifyClientWindowChange(Rectangle bounds) {
317     //we don't care about that as we don't display any composition windows
318     //do nothing
319   }
320 
321   /**
322    * Activates this input method
323    *
324    */
325   public void activate() {
326     enabled = true;
327     if(currentLocale == null) setLocale(defaultLocale);
328     if(mapVisible){
329       if(keyboardMap == null) keyboardMap = new KeyboardMap(this,
330                                                             currentHandler,
331                                                             currentState);
332       keyboardMap.addJob("SHOW");
333     }
334   }// activate()
335 
336   /**
337    * Deactivates this input method
338    *
339    * @param isTemporary
340    */
341   public void deactivate(boolean isTemporary) {
342     endComposition();
343     enabled = false;
344 //    if(mapVisible) keyboardMap.addJob("HIDE");
345   }// deactivate(boolean isTemporary)
346 
347   /**
348    * Hides all the windows displayed by the input method. Currently this only
349    * includes the virtual keyboard map.
350    *
351    */
352   public void hideWindows() {
353     if(mapVisible) keyboardMap.addJob("HIDE");
354   }
355 
356   /**
357    * Called by the system when a client unregisters to this input method. This
358    * event is currently ignored by the input method.
359    *
360    */
361   public void removeNotify() {
362     //so what! :)
363   }
364 
365   /**
366    * Ends the curent composition.
367    *
368    */
369   public void endComposition() {
370 //System.out.println("GateIM endComposition()!");
371     if(composedText.length() > 0 && currentState.isFinal()){
372       myContext.dispatchInputMethodEvent(
373             InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
374             (new AttributedString(composedText)).getIterator(),
375             composedText.length(), null, null);
376     }
377     composedText = "";
378   }
379 
380   /**
381    * Disposes this input method releasing all the memory.
382    *
383    */
384   public void dispose() {
385     endComposition();
386     if(keyboardMap != null){
387       keyboardMap.addJob("DIE");
388       keyboardMap = null;
389     }
390     currentLocale = null;
391     currentHandler = null;
392     currentState = null;
393     myContext = null;
394     supportedLocales.clear();
395     supportedLocales = null;
396     loadedLocales.clear();
397     loadedLocales = null;
398   }
399 
400   /**
401    * Gives the clients a chance to control the bevaviour of this input method
402    * by returning a handle to itself.
403    *
404    * @return a reference to this input method
405    */
406   public Object getControlObject() {
407     return this;
408   }
409 
410   /**
411    * Should the virtual keyboard map be visible?
412    *
413    * @param mapvis
414    */
415   public void setMapVisible(boolean mapvis) {
416     if(mapvis){
417       mapVisible = true;
418       if(keyboardMap == null) keyboardMap = new KeyboardMap(this,
419                                                             currentHandler,
420                                                             currentState);
421       keyboardMap.addJob("SHOW");
422     }else{
423       mapVisible = false;
424       if(keyboardMap != null) keyboardMap.addJob("HIDE");
425     }
426   }// setMapVisible(boolean mapvis)
427 
428   /**
429    * Loads a new locale if it's not already loaded.
430    *
431    * @param locale
432    */
433   protected void loadLocale(Locale locale){
434     String fileName = (String)supportedLocales.get(locale);
435     if(fileName == null) throw new IllegalArgumentException(
436                                    "Unknown locale: " + locale);
437     currentHandler = (LocaleHandler)loadedLocales.get(locale);
438     if(currentHandler == null){
439       try {
440         currentHandler = new LocaleHandler(locale, fileName);
441         loadedLocales.put(locale, currentHandler);
442         currentState = currentHandler.getInitialState();
443       } catch(IOException ioe) {
444         throw new IllegalArgumentException("Cannot load locale: " + locale);
445       }
446     }// if
447   }// loadLocale(Locale locale)
448 
449   /**
450    * Returns theinput context for this input method.
451    *
452    */
453   public InputMethodContext getContext(){
454     return myContext;
455   }
456 
457   //--------- variables
458   /**
459    * The active locale
460    *
461    */
462   Locale currentLocale;
463   /**
464    * The default locale to be used when this method is loaded and no locale is
465    * specified.
466    *
467    */
468   Locale defaultLocale = new Locale("en",  "", "ASCII");
469   /**
470    * The current locale handler.
471    *
472    */
473   LocaleHandler currentHandler;
474   /**
475    * The input context
476    *
477    */
478   InputMethodContext myContext;
479   //maps from Loacle to String (the file name)
480   /**
481    * The available locales (the locales for which a definition file exists).
482    *
483    */
484   Map supportedLocales;
485   //maps from Locale to LocaleHandler
486   /**
487    * The locales that have been loaded already. Maps from Loacle to
488    * {@link LocaleHandler}.
489    *
490    */
491   Map loadedLocales;
492   /**
493    * Is this inpuit method enabled?
494    *
495    */
496   boolean enabled;
497   /**
498    * The composed text;
499    *
500    */
501   String composedText = "";
502   /**
503    * The current state of the current LocaleHandler.
504    *
505    */
506   State currentState;
507   /**
508    * Not used
509    *
510    */
511   Map additionalKeymaps;
512   /**
513    * The current virtual keyboard map.
514    *
515    */
516   static KeyboardMap keyboardMap;
517   /**
518    * Should the keyboard map be visible?
519    *
520    */
521   boolean mapVisible = true;
522 
523   /** The resource path to the input methods director
524    */
525   static private String imBase =  "/guk/im/data/";
526 
527   /** Sets the default path to be used when looking for input methods.
528    * This should be  a resource path (a path inside the class path).
529    * By default the path is &quot;guk/im/data/&quot;
530    *
531    * @param path
532    */
533   static public void setIMBase(String path){
534     imBase = path;
535   }
536 
537   /** Gets the path inside the classpath where the input methods should be found
538    */
539   public static String getIMBase(){return imBase;}
540 
541 
542   static private Font keyboardFont = new Font("Arial Unicode MS", Font.PLAIN, 12);
543   public static Font getKeyboardFont(){
544     return keyboardFont;
545   }
546 }// class GateIM implements InputMethod
547