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