001: /*
002: * *
003: * Copyright 1990-2007 Sun Microsystems, Inc. All Rights Reserved.
004: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
005: *
006: * This program is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU General Public License version
008: * 2 only, as published by the Free Software Foundation.
009: *
010: * This program is distributed in the hope that it will be useful, but
011: * WITHOUT ANY WARRANTY; without even the implied warranty of
012: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013: * General Public License version 2 for more details (a copy is
014: * included at /legal/license.txt).
015: *
016: * You should have received a copy of the GNU General Public License
017: * version 2 along with this work; if not, write to the Free Software
018: * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
019: * 02110-1301 USA
020: *
021: * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
022: * Clara, CA 95054 or visit www.sun.com if you need additional
023: * information or have any questions.
024: */
025: package com.sun.midp.chameleon.input;
026:
027: import javax.microedition.lcdui.Canvas;
028: import javax.microedition.lcdui.TextField;
029: import javax.microedition.lcdui.Displayable;
030: import com.sun.midp.i18n.*;
031: import com.sun.midp.log.Logging;
032: import com.sun.midp.log.LogChannels;
033:
034: /**
035: * An InputMode instance which processes the numeric 0-9 keys
036: * as their literal numeric values.
037: */
038: abstract class BasicInputMode implements InputMode, Runnable {
039: /**
040: * Text input constraints for this input mode. The semantics of the
041: * constraints value are defined in the TextField API.
042: */
043: protected int constraints;
044:
045: /**
046: * Text input modifiers for this input mode. The semantics of the
047: * modifiers value are defined in the TextField API.
048: */
049: protected int modifiers;
050:
051: /**
052: * A timeout, in ms, after which a pending key will be committed
053: * to the text component. By default this is set to 250ms.
054: */
055: protected static final int KEY_COMMIT_TIMEOUT = 1000;
056:
057: /** A holder for the keyCode which was last processed */
058: protected int lastKey = -1;
059:
060: /** The InputModeMediator for the current input session */
061: protected InputModeMediator mediator;
062:
063: /**
064: * A boolean flag used by the timer in this input mode to
065: * determine if a character should be committed to the text
066: * component or not
067: */
068: protected boolean commitChar;
069:
070: /**
071: * The number of times the user pressed the last key code. This value
072: * acts as an index into the array of possible characters of any one
073: * key. For example, a lastKey == to Canvas.KEY_NUM2 and a clickCount
074: * of 3 would yield a 'c'.
075: */
076: protected int clickCount;
077:
078: /**
079: * The single, pending character based on the key presses thus far
080: */
081: protected int pendingChar = KEYCODE_NONE;
082:
083: /**
084: * Flag indicating if more matches exist
085: */
086: protected boolean hasMoreMatches;
087:
088: /**
089: * A boolean flag used by the timer in this input mode to
090: * know when to completely shut down the timer thread. That is,
091: * when the input session is no longer active, the timer thread
092: * in this input mode will quit entirely, freeing up system
093: * resources.
094: */
095: protected boolean sessionIsLive;
096:
097: /** the possible key maps for this input mode */
098: static char[][] keyMap;
099:
100: /**
101: * This method will be called before any input keys are passed
102: * to this InputMode to allow the InputMode to perform any needed
103: * initialization. A reference to the InputModeMediator which is
104: * currently managing the relationship between this InputMode and
105: * the input session is passed in. This reference can be used
106: * by this InputMode to commit text input as well as end the input
107: * session with this InputMode. The reference is only valid until
108: * this InputMode's endInput() method is called.
109: *
110: * @param constraints text input constraints. The semantics of the
111: * constraints value are defined in the TextField API.
112: *
113: * @param mediator the InputModeMediator which is negotiating the
114: * relationship between this InputMode and the input session
115: * @param inputSubset current input subset
116: */
117: public void beginInput(InputModeMediator mediator,
118: String inputSubset, int constraints) {
119: if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
120: Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
121: "[basic.beginInput] >>");
122: }
123: validateState(false);
124: this .mediator = mediator;
125: this .constraints = constraints & TextField.CONSTRAINT_MASK;
126: this .modifiers = constraints & ~TextField.CONSTRAINT_MASK;
127: startTimer();
128: setInputSubset(inputSubset);
129: setKeyMap(constraints, false);
130: if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
131: Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
132: "[basic.beginInput] <<");
133: }
134: }
135:
136: /**
137: * Start timmer which will attempt to commit
138: * a pending character after a certain timeout
139: */
140: private void startTimer() {
141: (new Thread(this )).start();
142: }
143:
144: /**
145: * Stop timmer which will attempt to commit
146: * a pending character after a certain timeout
147: */
148: private void stopTimer() {
149: sessionIsLive = false;
150: // Lastly, we'll interrupt the timer to end it.
151: synchronized (this ) {
152: try {
153: notify();
154: } catch (IllegalMonitorStateException ignore) {
155: }
156: }
157: }
158:
159: /**
160: * Reset timmer which will attempt to commit
161: * a pending character after a certain timeout
162: */
163: private void resetTimer() {
164: sessionIsLive = true;
165: // Lastly, we'll interrupt the timer to end it.
166: synchronized (this ) {
167: try {
168: notify();
169: } catch (IllegalMonitorStateException ignore) {
170: }
171: }
172: }
173:
174: /**
175: * Returns true if input mode is using its own displayable, false ifinput
176: * mode does not require the speial displayable for its representation.
177: * By default - false
178: * @return true if input mode is using its own displayable, otherwise false
179: */
180: public boolean hasDisplayable() {
181: return false;
182: }
183:
184: /**
185: * Converts the string to key map. The rows are separated each from other
186: * by '$'. The characters inside of one row follow each to other without
187: * any separator.
188: * @param line string combines all keys
189: * @return map of the keys in char[][] format
190: */
191: protected char[][] getMapByLine(String line) {
192: char[] chars = line.toCharArray();
193: int rows = 1;
194: for (int i = line.length() - 1; i >= 0; i--) {
195: if (chars[i] == '$')
196: rows++;
197: }
198:
199: char[][] map = new char[rows][];
200: for (int start = 0, j = 0; start < line.length(); j++) {
201: int end = line.indexOf('$', start);
202:
203: // if '$' is not found that means the end of string is reached
204: if (end == -1)
205: end = line.length();
206: map[j] = line.substring(start, end).toCharArray();
207: start = end + 1;
208: }
209: return map;
210: }
211:
212: /**
213: * Set the corresponding key map.
214: *
215: * @param constraints text input constraints. The semantics of the
216: * constraints value are defined in the TextField API.
217: *
218: * @param longPress return true if it's long key press otherwise false
219: *
220: * @return true if the key map has been changed otherwise false
221: */
222: protected abstract boolean setKeyMap(int constraints,
223: boolean longPress);
224:
225: /**
226: * Process the given key code as input.
227: *
228: * This method will return true if the key was processed successfully,
229: * false otherwise.
230: *
231: * @param keyCode the keycode of the key which was input
232: * @param longPress return true if it's long key press otherwise false
233: * @return the key code if the key has been committed for the input, or
234: * KEYCODE_NONE if the key has not been habdled by the input mode, or
235: * KEYCODE_INVISIBLE if the key has been handled by the input mode but
236: * this key has not been displayed
237: */
238: public int processKey(int keyCode, boolean longPress) {
239: int ret = KEYCODE_NONE;
240: if (isValidKey(keyCode, longPress)) {
241:
242: // We immediately disable the commit of any pending character
243: // input in case the timer expires
244: commitChar = false;
245: validateState(true);
246:
247: if (mediator != null && mediator.isClearKey(keyCode)
248: || keyCode == Canvas.LEFT
249: || keyCode == Canvas.RIGHT || keyCode == Canvas.UP
250: || keyCode == Canvas.DOWN) {
251: if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
252: Logging.report(Logging.INFORMATION,
253: LogChannels.LC_HIGHUI,
254: "[processKey] got clear or arrow. lastKey="
255: + lastKey);
256: }
257: completeInputMode(true);
258: } else {
259:
260: if (setKeyMap(constraints, longPress)) {
261: pendingChar = KEYCODE_NONE;
262: clickCount = 0;
263: }
264:
265: // at first check if previous key has to be committed
266:
267: // If we have a pending keycode and this new keycode is
268: // different, we will commit the previous key and continue
269: if (lastKey != -1 && lastKey != keyCode) {
270: commitPendingChar();
271: }
272:
273: clickCount++;
274:
275: // If the pending key code has just one match or long key
276: // press happens commit the current key
277:
278: if (longPress) {
279: if (lastKey != -1) {
280: lastKey = keyCode;
281: commitPendingChar();
282: }
283: } else if (hasOneCase(keyCode)) {
284: lastKey = keyCode;
285: commitPendingChar();
286: } else {
287: lastKey = keyCode;
288: }
289:
290: // Lastly, we'll interrupt the timer to reset it or start it if
291: // timer is still not working.
292: resetTimer();
293:
294: if (getNextChar() == KEYCODE_NONE) {
295: lastKey = -1;
296: }
297:
298: ret = getPendingCharInternal();
299: }
300: } else {
301: ret = InputMode.KEYCODE_INVISIBLE;
302: if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
303: Logging.report(Logging.INFORMATION,
304: LogChannels.LC_HIGHUI,
305: "[processKey] returning KEYCODE_INVISIBLE");
306: }
307: }
308: return ret;
309: }
310:
311: /**
312: * Check if the key has to be handled this input mode
313: * @param keyCode key
314: * @param longPress true if long key press happens otherwise false.
315: * @return true if this key can be handled by this input mode,
316: * otherwise false
317: */
318: private boolean isValidKey(int keyCode, boolean longPress) {
319: if ((keyCode != Canvas.KEY_STAR && keyCode != Canvas.KEY_POUND
320: && (mediator != null && !mediator.isClearKey(keyCode))
321: && keyCode != Canvas.LEFT && keyCode != Canvas.RIGHT
322: && keyCode != Canvas.UP && keyCode != Canvas.DOWN && (keyCode < Canvas.KEY_NUM0 || keyCode > Canvas.KEY_NUM9))
323: || (longPress && lastKey != keyCode && lastKey != -1)) {
324: if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
325: Logging.report(Logging.INFORMATION,
326: LogChannels.LC_HIGHUI, "INVALID KEY");
327: }
328: return false;
329: }
330: return true;
331: }
332:
333: /**
334: * Set the next capital mode for this input method
335: */
336: protected void nextCapsMode() {
337: }
338:
339: /**
340: * Get next possble char
341: * @return next key code
342: */
343: protected int getNextChar() {
344: pendingChar = KEYCODE_NONE;
345: return getPendingCharInternal();
346: }
347:
348: /**
349: * Return the next possible match for the key input processed thus
350: * far by this InputMode. A call to this method should be preceeded
351: * by a check of hasMoreMatches(). If the InputMode has more available
352: * matches for the given input, this method will return them one by one.
353: *
354: * @return a String representing the next available match to the key
355: * input thus far, or 'null' if no pending input is available
356: */
357: public String getNextMatch() {
358: String value = null;
359:
360: int ch = getNextChar();
361: if (ch != KEYCODE_NONE) {
362: value = String.valueOf((char) ch);
363: }
364: hasMoreMatches = false;
365:
366: return value;
367: }
368:
369: /**
370: * True, if after processing a key, there is more than one possible
371: * match to the input. If this method returns true, the getNextMatch()
372: * method can be called to return the value.
373: *
374: * @return true if after processing a key, there is more than the one
375: * possible match to the given input
376: */
377: public boolean hasMoreMatches() {
378: return hasMoreMatches;
379: }
380:
381: /**
382: * Gets the possible string matches
383: *
384: * @return returns the set of options.
385: */
386: public String[] getMatchList() {
387: // String[] value = null;
388: // int ch = getPendingCharInternal();
389:
390: // if (ch != KEYCODE_NONE) {
391: // value = new String[1];
392: // value[0] = String.valueOf((char)ch);
393: // } else {
394: // value = new String[0];
395: // }
396: // return value;
397: //
398: return new String[0];
399: }
400:
401: /**
402: * Mark the end of this InputMode's processing. The only possible call
403: * to this InputMode after a call to endInput() is a call to beginInput()
404: * to begin a new input session.
405: */
406: public void endInput() {
407: if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
408: Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
409: "[basic.endInput]");
410: }
411: validateState(true);
412: this .mediator = null;
413: clickCount = 0;
414: lastKey = -1;
415:
416: stopTimer();
417: }
418:
419: /**
420: * By default the regular input method has no specific displayable
421: * representation so it returns null.
422: * @return null by default
423: */
424: public Displayable getDisplayable() {
425: return null;
426: }
427:
428: /**
429: * Implementation of a timer routine which will attempt to commit
430: * a pending character after a certain timeout (depending on the
431: * state of the commitChar boolean).
432: */
433: public void run() {
434: if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
435: Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
436: "[run] sessionIsLive=" + sessionIsLive
437: + " commitChar=" + commitChar);
438: }
439: // We initially block until the first key press is processed
440: if (!sessionIsLive) {
441: try {
442: synchronized (this ) {
443: wait();
444: }
445: } catch (Throwable t) {
446: } // ignore interruptions
447: }
448:
449: while (sessionIsLive) {
450: try {
451: synchronized (this ) {
452: // Just before we start the timeout we set the
453: // commit flag to true. If it doesn't get reset
454: // to false by the processKey method, we will
455: // commit the pending key when we wake up
456: commitChar = true;
457: wait(KEY_COMMIT_TIMEOUT);
458: }
459: } catch (Throwable t) {
460: } // ignore any exceptions here
461:
462: if (sessionIsLive && commitChar) {
463: completeInputMode(true);
464: }
465: }
466: }
467:
468: /**
469: * This method will validate the state of this InputMode. If this
470: * is a check for an "active" operation, the TextInputMediator must
471: * be non-null or else this method will throw an IllegalStateException.
472: * If this is a check for an "inactive" operation, then the
473: * TextInputMediator should be null.
474: * @param activeOperation true if any operation is active otherwise false.
475: */
476: protected void validateState(boolean activeOperation) {
477: if (activeOperation && this .mediator == null) {
478: throw new IllegalStateException(
479: "Illegal operation on an input session already in progress");
480: } else if (!activeOperation && this .mediator != null) {
481: throw new IllegalStateException(
482: "Illegal operation on an input session which is not in progress");
483: }
484: }
485:
486: /**
487: * return the pending char for internal use
488: *
489: * @return return the pending char
490: */
491: public int getPendingCharInternal() {
492: if (pendingChar == KEYCODE_NONE) {
493: char[] chars = null;
494: char c;
495: // log("[basic.getPendingCharInternal] lastKey=" + lastKey);
496: if (lastKey == -1 || clickCount <= 0) {
497: if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
498: Logging
499: .report(Logging.INFORMATION,
500: LogChannels.LC_HIGHUI,
501: "[getPendingCharInternal] returning KEYCODE_NONE");
502: }
503: } else {
504: chars = getCharOptions(lastKey);
505: if (chars == null) {
506: if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
507: Logging
508: .report(Logging.INFORMATION,
509: LogChannels.LC_HIGHUI,
510: "[getPendingCharInternal] returning KEYCODE_NONE");
511: }
512: } else {
513: if (clickCount > chars.length) {
514: clickCount = 1;
515: }
516:
517: if (chars.length > 0) {
518: pendingChar = chars[clickCount - 1];
519: }
520:
521: hasMoreMatches = true;
522: if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
523: Logging.report(Logging.INFORMATION,
524: LogChannels.LC_HIGHUI,
525: "[getPendingCharInternal] returning "
526: + pendingChar);
527: }
528: }
529: }
530: }
531: return pendingChar;
532: }
533:
534: /**
535: * Check if only one char option exists for the key code
536: * @param keyCode key code
537: * @return true if only one char option exists otherwise false.
538: */
539: private boolean hasOneCase(int keyCode) {
540: boolean ret = false;
541: if (keyCode != -1) {
542: char[] options = getCharOptions(keyCode);
543: if (options != null)
544: ret = options.length <= 1;
545: }
546: return ret;
547: }
548:
549: /**
550: * return the pending char
551: * used to bypass the asynchronous commit mechanism
552: * e.g. to immediately commit a char before moving the cursor
553: *
554: * @return return the pending char
555: */
556: public char getPendingChar() {
557: int code = getPendingCharInternal();
558: char c = code == KEYCODE_NONE ? 0 : (char) code;
559: if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
560: Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
561: "[getPendingChar] returning " + c);
562: }
563: return c;
564: }
565:
566: /**
567: * Gets the possible matches for the key code
568: *
569: * @param lastKey the key code
570: *
571: * @return returns the set of options. Return null if matches are not found.
572: */
573: protected abstract char[] getCharOptions(int lastKey);
574:
575: /**
576: * This method is used to immediately commit the pending
577: * character because a new character is now pending.
578: *
579: * @return true if char has been committed otherwise false
580: */
581: protected boolean commitPendingChar() {
582: boolean committed = false;
583: int c = getPendingCharInternal();
584: if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
585: Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
586: "[commitPendingChar] getPendingChar=" + c);
587: }
588: if (c != KEYCODE_NONE) {
589: if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
590: Logging.report(Logging.INFORMATION,
591: LogChannels.LC_HIGHUI,
592: "[commitPendingChar] commiting "
593: + String.valueOf((char) c));
594: }
595: committed = true;
596: mediator.commit(String.valueOf((char) c));
597: }
598:
599: lastKey = -1;
600: clickCount = 0;
601: pendingChar = KEYCODE_NONE;
602: return committed;
603: }
604:
605: /**
606: * This method is used to immediately commit the given
607: * string and then call the TextInputMediator's inputModeCompleted()
608: * method
609: * @param commit true if the char is accepted, false if the char is rejected
610: */
611: protected void completeInputMode(boolean commit) {
612: if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
613: Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
614: "[Basic.completeInputMode] commit = " + commit);
615: }
616: if (commit) {
617: commitPendingChar();
618: }
619:
620: clickCount = 0;
621: lastKey = -1;
622:
623: stopTimer();
624: startTimer();
625: }
626:
627: /**
628: * Notify about current input subset
629: * @param inputSubset current input subset
630: */
631: protected void setInputSubset(String inputSubset) {
632: }
633:
634: /**
635: * This method is called to determine if this InputMode supports
636: * the given text input constraints. The semantics of the constraints
637: * value are defined in the javax.microedition.lcdui.TextField API.
638: * If this InputMode returns false, this InputMode must not be used
639: * to process key input for the selected text component.
640: *
641: * @param constraints text input constraints. The semantics of the
642: * constraints value are defined in the TextField API.
643: *
644: * @return true if this InputMode supports the given text component
645: * constraints, as defined in the MIDP TextField API
646: */
647: abstract public boolean supportsConstraints(int constraints);
648:
649: /**
650: * Returns the display name which will represent this InputMode to
651: * the user, such as in a selection list or the softbutton bar.
652: *
653: * @return the locale-appropriate name to represent this InputMode
654: * to the user
655: */
656: abstract public String getName();
657:
658: /**
659: * Returns the command name which will represent this InputMode in
660: * the input menu
661: *
662: * @return the locale-appropriate name to represent this InputMode
663: * to the user
664: */
665: abstract public String getCommandName();
666:
667: /**
668: * Returns the map specifying this input mode is proper one for the
669: * particular pair of input subset and constraint. The form of the map is
670: *
671: * |ANY|EMAILADDR|NUMERIC|PHONENUMBER|URL|DECIMAL|
672: * ---------------------------------------------------------------------
673: * IS_FULLWIDTH_DIGITS |t|f| t|f | t|f | t|f |t|f| t|f |
674: * IS_FULLWIDTH_LATIN |t|f| t|f | t|f | t|f |t|f| t|f |
675: * IS_HALFWIDTH_KATAKANA |t|f| t|f | t|f | t|f |t|f| t|f |
676: * IS_HANJA |t|f| t|f | t|f | t|f |t|f| t|f |
677: * IS_KANJI |t|f| t|f | t|f | t|f |t|f| t|f |
678: * IS_LATIN |t|f| t|f | t|f | t|f |t|f| t|f |
679: * IS_LATIN_DIGITS |t|f| t|f | t|f | t|f |t|f| t|f |
680: * IS_SIMPLIFIED_HANZI |t|f| t|f | t|f | t|f |t|f| t|f |
681: * IS_TRADITIONAL_HANZI |t|f| t|f | t|f | t|f |t|f| t|f |
682: * MIDP_UPPERCASE_LATIN |t|f| t|f | t|f | t|f |t|f| t|f |
683: * MIDP_LOWERCASE_LATIN |t|f| t|f | t|f | t|f |t|f| t|f |
684: * NULL |t|f| t|f | t|f | t|f |t|f| t|f |
685: *
686: * @return input subset x constraint map
687: */
688: abstract public boolean[][] getIsConstraintsMap();
689: }
|