/*
*
*
* Copyright 1990-2007 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version
* 2 only, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License version 2 for more details (a copy is
* included at /legal/license.txt).
*
* You should have received a copy of the GNU General Public License
* version 2 along with this work; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
* Clara, CA 95054 or visit www.sun.com if you need additional
* information or have any questions.
*/
package javax.microedition.lcdui;
import com.sun.midp.lcdui.EventConstants;
import com.sun.midp.lcdui.PhoneDial;
import com.sun.midp.lcdui.DynamicCharacterArray;
import com.sun.midp.lcdui.Text;
import com.sun.midp.lcdui.TextCursor;
import com.sun.midp.lcdui.TextPolicy;
import com.sun.midp.log.Logging;
import com.sun.midp.log.LogChannels;
import com.sun.midp.chameleon.*;
import com.sun.midp.chameleon.input.*;
import com.sun.midp.chameleon.layers.InputModeLayer;
import com.sun.midp.chameleon.layers.PTILayer;
import com.sun.midp.chameleon.skins.ScreenSkin;
import com.sun.midp.chameleon.skins.TextFieldSkin;
import com.sun.midp.chameleon.skins.resources.TextFieldResources;
import com.sun.midp.chameleon.skins.resources.PTIResources;
import com.sun.midp.chameleon.skins.resources.InputModeResources;
import com.sun.midp.configurator.Constants;
import java.util.*;
/**
* This is the look &s; feel implementation for TextField.
*/
class TextFieldLFImpl extends ItemLFImpl implements
TextFieldLF, TextInputComponent, CommandListener
{
/** TextField instance associated with this view */
protected TextField tf;
/** cursor to keep track of where to draw the cursor */
protected TextCursor cursor;
/** editable state of the text field */
protected boolean editable;
/** flag indicating the first time traverse is executed */
protected boolean firstTimeInTraverse = true;
/** The character set to use as the initial input mode, may be null */
protected String initialInputMode;
/** The input mode cause the traverse out for the text component */
protected InputMode interruptedIM;
/**
* This SubMenuCommand holds the set of InputModes available on the
* InputMode pull-out menu
*/
protected SubMenuCommand inputMenu;
/** The TextInputMediator which handles translating key presses */
protected static TextInputSession inputSession;
/** The PopupLayer that represents open state of this ChoiceGroup POPUP. */
protected static PTILayer pt_popup;
/**
* The set of InputModes available to process this text component
*/
protected InputMode[] inputModes;
/**
* A special "popup" layer that shows the user an indicator of
* what the currently selected input mode is
*/
protected InputModeLayer inputModeIndicator;
/** The state of the popup ChoiceGroup (false by default) */
private boolean pt_popupOpen;
/** predictive text options */
String[] pt_matches;
/**
* A four dimensional array holding the anchor point, item
* height, and space below the item which corresponds to the
* parameters to the InputModeLayer's setAnchor() method
*/
protected int[] inputModeAnchor;
/**
* This is a flag to turn on the input mode indicator popup
*/
protected boolean showIMPopup;
/**
* true if the preferredX field in TextCursor should be updated with
* the latest TextCursor.x coordinate
*/
protected boolean usePreferredX = true;
/**
* pixel offset to the start of the text field (for example, if
* xScrollOffset is -60 it means means that the text in this
* text field is scrolled 60 pixels left of the left edge of the
* text field)
*/
protected int xScrollOffset;
/**
* Total width of the text contained in this TextField
*/
protected int textWidth;
/**
* Width of the scroll area for text
*/
protected int scrollWidth;
/** A Timer which will handle firing repaints of the ScrollPainter */
protected static Timer textScrollTimer;
/** A TimerTask which will repaint scrolling text on a repeated basis */
protected TextScrollPainter textScrollPainter;
/** flag indicating the pointer press event is happened but release is
still not handled */
private boolean pressedIn = false;
/**
* Creates TextFieldLF for the passed in TextField.
* @param tf The TextField associated with this TextFieldLF
*/
TextFieldLFImpl(TextField tf) {
super(tf);
TextFieldResources.load();
PTIResources.load();
InputModeResources.load();
this.tf = tf;
cursor = new TextCursor(tf.buffer.length());
cursor.visible = false;
xScrollOffset = 0;
if (inputSession == null) {
inputSession = new BasicTextInputSession();
pt_popup = new PTILayer(inputSession);
}
lSetConstraints();
if (textScrollTimer == null) {
textScrollTimer = new Timer();
}
inputModeIndicator = new InputModeLayer();
inputModeAnchor = new int[4];
}
// *****************************************************
// Public methods defined in interfaces
// *****************************************************
/**
* Update the character buffer in TextField with pending user input.
* Since Java TextField always keeps TextField.buffer up-to-date,
* there is no pending user input. Do nothing but return false here.
* @return true if there is new user input updated in the buffer.
*/
public boolean lUpdateContents() {
return false; // nothing pending
}
/**
* Notifies L&F of a content change in the corresponding TextField.
*/
public void lSetChars() {
cursor.index = tf.buffer.length(); // cursor at the end
cursor.option = Text.PAINT_USE_CURSOR_INDEX;
if (!editable) {
resetUneditable();
}
lRequestPaint();
}
/**
* Notifies L&s;F of a character insertion in the corresponding
* TextField.
* @param data the source of the character data
* @param offset the beginning of the region of characters copied
* @param length the number of characters copied
* @param position the position at which insertion occurred
*/
public void lInsert(char data[], int offset, int length, int position) {
if (data == null) {
return; // -au
}
if (position <= cursor.index) {
cursor.index += length;
cursor.option = Text.PAINT_USE_CURSOR_INDEX;
}
if (!editable) {
resetUneditable();
}
if (item.owner == null) {
return; // because owner is null, we just return.
}
lRequestPaint();
}
/**
* Notifies L&amsp;F of character deletion in the corresponding
* TextField.
* @param offset the beginning of the deleted region
* @param length the number of characters deleted
*/
public void lDelete(int offset, int length) {
if (cursor.index >= offset) {
int diff = cursor.index - offset;
cursor.index -= (diff < length) ? diff : length;
cursor.option = Text.PAINT_USE_CURSOR_INDEX;
}
if (!editable) {
resetUneditable();
}
if (item.owner == null) {
return; // because owner is null, we just return.
}
lRequestPaint();
}
/**
* Notifies L&s;F of a maximum size change in the corresponding
* TextField.
* @param maxSize - the new maximum size
*/
public void lSetMaxSize(int maxSize) {
int max = tf.getMaxSize();
if (cursor.index > max) {
cursor.index = max;
}
lRequestInvalidate(true, true);
}
/**
* Returns the available size (number of characters) that can be
* stored in this <code>TextInputComponent</code>.
* @return available size in characters
*/
public int getAvailableSize() {
return tf.getMaxSize() - tf.buffer.length();
}
/**
* Gets the current input position.
* @return the current caret position, <code>0</code> if at the beginning
*/
public int lGetCaretPosition() {
return cursor.index;
}
/**
* Update states associated with input constraints.
*
* @param autoScrolling true if auto scrolling is allowed
* to show large uneditable contents
*/
void setConstraintsCommon(boolean autoScrolling) {
// Cleanup old states that are constraints sensitive
if (hasFocus && visible) {
if (editable) {
disableInput();
} else if (autoScrolling) {
stopScroll();
}
}
// Change editability
editable =
(tf.constraints & TextField.UNEDITABLE) != TextField.UNEDITABLE;
// Setup new states that are constraints sensitive
if (hasFocus && visible) {
if (editable) {
enableInput();
} else if (autoScrolling) {
startScroll();
}
}
}
/**
* Notifies L&s;F that constraints have to be changed.
*/
public void lSetConstraints() {
setConstraintsCommon(true);
// The layout might change if the constraints does not match
// the current text, causing the text to be set empty,
// or changed to "password", causing it to change width
// Request relayout to based on updated contentSize
if (item.owner == null) {
return; // because owner is null, we just return.
}
lRequestInvalidate(true, true);
}
/**
* Validate a given character array against a constraints.
*
* @param buffer a character array
* @param constraints text input constraints
* @return true if constraints is met by the character array
*/
public boolean lValidate(DynamicCharacterArray buffer, int constraints) {
return TextPolicy.isValidString(buffer, constraints);
}
/**
* Notifies L&s;F that preferred initial input mode was changed.
* @param characterSubset a string naming a Unicode character subset,
* or <code>null</code>
*/
public void lSetInitialInputMode(String characterSubset) {
this.initialInputMode = characterSubset;
}
/**
* Notifies item that it has been recently deleted
* Traverse out the textFieldLF. This implicitly remove the InputMode
* indicator and possibly the Predictive Text Input indicator from the
* screen.
*/
public void itemDeleted() {
uCallTraverseOut();
}
// CommandListener interface
/**
* This CommandListener is only for the selection of Commands on
* the Input sub-menu, which lists each of the available InputModes
* for this text component.
* @param c command
* @param d displayable
*/
public void commandAction(Command c, Displayable d) {
Command[] inputCommands = inputMenu.getSubCommands();
String label = c.getLabel();
if (inputCommands != null && label != null) {
for (int i = 0; i < inputCommands.length; i++) {
if (label.equals(inputCommands[i].getLabel())) {
inputSession.setCurrentInputMode(inputModes[i]);
break;
}
}
}
}
// *****************************************************
// Package private methods
// *****************************************************
/**
* Indicate whether or not traversing should occur.
*
* @return <code>false</code> always
*/
boolean shouldSkipTraverse() {
return false;
}
/**
* Sets the content size in the passed in array.
* Content is calculated based on the availableWidth.
* size[WIDTH] and size[HEIGHT] should be set by this method.
* @param size The array that holds Item content size and location
* in Item internal bounds coordinate system.
* @param availableWidth The width available for this Item
*/
void lGetContentSize(int size[], int availableWidth) {
Font f = ScreenSkin.FONT_INPUT_TEXT;
size[HEIGHT] = f.getHeight() + (2 * TextFieldSkin.PAD_V);
size[WIDTH] = f.charWidth('W') * tf.buffer.capacity() +
(2 * TextFieldSkin.PAD_H);
if (size[WIDTH] > availableWidth) {
size[WIDTH] = availableWidth;
}
// update scrollWidth used in scrolling UE text
scrollWidth = size[WIDTH] - (2 * TextFieldSkin.PAD_H) - 1;
}
/**
* Determine if this Item should have a newline after it
*
* @return true if it should have a newline after
*/
boolean equateNLA() {
if (super.equateNLA()) {
return true;
}
return ((tf.layout & Item.LAYOUT_2) != Item.LAYOUT_2);
}
/**
* Determine if this Item should have a newline before it
*
* @return true if it should have a newline before
*/
boolean equateNLB() {
if (super.equateNLB()) {
return true;
}
return ((tf.layout & Item.LAYOUT_2) != Item.LAYOUT_2);
}
/**
* Paints the content area of this TextField.
* Graphics is translated to contents origin.
* @param g The graphics where Item content should be painted
* @param width The width available for the Item's content
* @param height The height available for the Item's content
*/
void lPaintContent(Graphics g, int width, int height) {
// Draw the TextField background region
if (editable) {
if (TextFieldSkin.IMAGE_BG != null) {
CGraphicsUtil.draw9pcsBackground(g, 0, 0, width, height,
TextFieldSkin.IMAGE_BG);
} else {
// draw widget instead of using images
CGraphicsUtil.drawDropShadowBox(g, 0, 0, width, height,
TextFieldSkin.COLOR_BORDER,
TextFieldSkin.COLOR_BORDER_SHD,
TextFieldSkin.COLOR_BG);
}
} else {
if (TextFieldSkin.IMAGE_BG_UE != null) {
CGraphicsUtil.draw9pcsBackground(g, 0, 0, width, height,
TextFieldSkin.IMAGE_BG_UE);
} else {
// draw widget instead of using images
CGraphicsUtil.drawDropShadowBox(g, 0, 0, width, height,
TextFieldSkin.COLOR_BORDER_UE,
TextFieldSkin.COLOR_BORDER_SHD_UE,
TextFieldSkin.COLOR_BG_UE);
}
}
// We need to translate by 1 more pixel horizontally
// to reserve space for cursor in the empty textfield
g.clipRect(TextFieldSkin.PAD_H, TextFieldSkin.PAD_V,
width - (2 * TextFieldSkin.PAD_H),
height - (2 * TextFieldSkin.PAD_V));
g.translate(TextFieldSkin.PAD_H + 1,
TextFieldSkin.PAD_V);
int clr;
if (hasFocus) {
clr = (editable ? ScreenSkin.COLOR_FG_HL :
ScreenSkin.COLOR_FG_HL);
} else {
clr = (editable ? TextFieldSkin.COLOR_FG :
TextFieldSkin.COLOR_FG_UE);
}
xScrollOffset = paint(g, tf.buffer,
inputSession.getPendingChar(),
tf.constraints,
ScreenSkin.FONT_INPUT_TEXT, clr,
width - (2 * TextFieldSkin.PAD_H),
height - (2 * TextFieldSkin.PAD_V),
xScrollOffset, Text.NORMAL, cursor);
g.translate(-(TextFieldSkin.PAD_H + 1),
-TextFieldSkin.PAD_V);
if (usePreferredX) {
cursor.preferredX = cursor.x;
}
}
/**
* Returns the string that would be stored. This may not be the
* same as the string that is actually displayed because of the
* options argument that affects painting.
*
* @param dca the displayed text
* @param constraints text constraints
* @param cursor text cursor object to use to draw vertical bar
* @param modifyCursor true if this method can modify the cursor
* object if necessary, false otherwise
* @return the string that will be stored in buffer. This may not be what
* is actually drawn depending on the options. If it's
* impossible to get the actual string null is returned
*/
public String getBufferString(DynamicCharacterArray dca,
int constraints,
TextCursor cursor,
boolean modifyCursor) {
String ret = null;
if (!bufferedTheSameAsDisplayed(constraints)) {
DynamicCharacterArray out = new DynamicCharacterArray(dca.length());
if (!modifyCursor) {
cursor = new TextCursor(cursor);
}
if ((constraints & TextField.CONSTRAINT_MASK) ==
TextField.PHONENUMBER) {
for (int i = 0, j = 0; i < dca.length(); i++) {
char next = dca.charAt(i);
if (next == ' ') {
if (cursor.index > i) cursor.index--;
} else {
out.insert(j++, next);
}
}
}
ret = out.toString();
} else {
ret = dca.toString();
}
return ret;
}
/**
* Returns the string that would be painted. This may not be the
* same as the string that is actually displayed because of the
* options argument that affects painting.
*
* @param dca the text to paint
* @param opChar option char
* @param constraints text constraints
* @param cursor text cursor object to use to draw vertical bar
* @param modifyCursor true if this method can modify the cursor
* object if necessary, false otherwise
* @return the string that will be sent to be drawn. this may not be what
* is actually drawn depending on the options
*/
// IMPL NOTE: this is slow to call repeatedly...
// perhaps we cache a DisplayString?
public String getDisplayString(DynamicCharacterArray dca,
char opChar,
int constraints,
TextCursor cursor,
boolean modifyCursor) {
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
"[tf.getDisplayString]");
}
DynamicCharacterArray out = new DynamicCharacterArray(dca.length() + 1);
int index = cursor == null ? dca.length() : cursor.index;
if ((constraints & TextField.PASSWORD) == TextField.PASSWORD) {
index = getStringForPassword(dca, constraints, index, opChar, out);
} else { // not password
out.insert(dca.toCharArray(), 0, dca.length(), 0);
switch (constraints & TextField.CONSTRAINT_MASK) {
case TextField.PHONENUMBER:
index = getStringForPhoneNumber(dca, index, opChar, out);
break;
case TextField.DECIMAL:
index = getStringForDecimal(dca, index, opChar, out);
break;
case TextField.NUMERIC:
index = getStringForNumeric(dca, index, opChar, out);
break;
case TextField.EMAILADDR:
case TextField.URL:
case TextField.ANY:
if (opChar > 0 && dca.length() < tf.getMaxSize()) {
out.insert(index++, opChar);
}
break;
default:
// for safety/completeness.
Logging.report(Logging.ERROR, LogChannels.LC_HIGHUI,
"TextFieldLFImpl: constraints=" + constraints);
if (opChar > 0 && dca.length() < tf.getMaxSize()) {
out.insert(index++, opChar);
}
break;
}
}
if (out == null) {
out = dca;
}
if (modifyCursor && cursor != null) {
cursor.index = index;
}
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
"[TF.getDisplayString] getMatchList:");
}
pt_matches = inputSession.getMatchList();
return out.toString();
}
/**
* Returns the string that would be painted for the numeric constraints.
*
* @param dca the text to paint
* @param index cursor index
* @param opChar option char
* @param out the string that will be sent to be drawn.
* @return new cursor index
*/
private int getStringForNumeric(DynamicCharacterArray dca,
int index,
char opChar,
DynamicCharacterArray out) {
// change the number sign
if (' ' == opChar) {
if (dca.charAt(0) == '-') {
out.delete(0, 1);
if (index > 0) index--;
} else if (dca.length() < tf.getMaxSize()) {
out.insert(0, '-');
index++;
}
} else if (opChar > 0 && dca.length() < tf.getMaxSize()) {
out.insert(index++, opChar);
}
return index;
}
/**
* Returns the string that would be painted for the decimal constraints.
*
* @param dca the text to paint
* @param index cursor index
* @param opChar option char
* @param out the string that will be sent to be drawn.
* @return new cursor index
*/
private int getStringForDecimal(DynamicCharacterArray dca,
int index,
char opChar,
DynamicCharacterArray out) {
// it can be extended by '0' if '.' is added at the beginning
out.setCapacity(dca.length() + 1 + 1);
// change the number sign
if (' ' == opChar) {
if (dca.charAt(0) == '-') {
out.delete(0, 1);
if (index > 0) index--;
} else if (dca.length() < tf.getMaxSize()) {
out.insert(0, '-');
index++;
}
opChar = 0;
} else if ('.' == opChar && dca.length() < tf.getMaxSize()) {
/**
* insert the '.'. If it's inserted at the beginning of the number
* '0' has to be added before the '.' Number can not contain '.'
* twice
*/
char[] buf = dca.toCharArray();
int i = 0;
for (; i < dca.length(); i++) {
if (buf[i] == '.') break;
}
if (i == dca.length()) {
if (dca.charAt(0) == '-') {
if (index == 0) index++;
if (index == 1) out.insert(index++, '0');
} else if (index == 0) {
out.insert(index++, '0');
}
} else {
opChar = 0;
}
}
if (opChar > 0 && dca.length() < tf.getMaxSize()) {
out.insert(index++, opChar);
}
return index;
}
/**
* Returns the string that would be painted for the phone number constraints
*
* @param dca the text to paint
* @param index cursor index
* @param opChar option char
* @param out the string that will be sent to be drawn.
* @return new cursor index
*/
private int getStringForPhoneNumber(DynamicCharacterArray dca,
int index,
char opChar,
DynamicCharacterArray out) {
// +3 is the most characters we will need to insert here
out.setCapacity(dca.length() + 1 + 3);
switch (dca.length()) {
case 5:
case 6:
case 7:
if (out.length() < tf.getMaxSize()) {
out.insert(3, ' ');
if (index > 3) index++;
}
break;
case 11:
if (out.length() + 2 < tf.getMaxSize()) {
out.insert(1, ' ');
if (index > 1) index++;
out.insert(5, ' ');
if (index > 5) index++;
out.insert(9, ' ');
if (index > 9) index++;
}
break;
case 8:
case 9:
case 10:
default:
if (out.length() + 1 < tf.getMaxSize()) {
out.insert(3, ' ');
if (index > 3) index++;
out.insert(7, ' ');
if (index > 7) index++;
}
break;
case 0:
case 1:
case 2:
case 3:
case 4:
break;
}
if (opChar > 0 && out.length() + 1 < tf.getMaxSize()) {
out.insert(index++, opChar);
}
return index;
}
/**
* Returns the string that would be painted for the password modifier.
*
* @param dca the text to paint
* @param index cursor index
* @param opChar option char
* @param out the string that will be sent to be drawn.
* @param constraints text input constraints
* @return new cursor index
*/
private int getStringForPassword(DynamicCharacterArray dca,
int constraints,
int index,
char opChar,
DynamicCharacterArray out) {
for (int i = 0; i < dca.length(); i++) {
out.append('*');
}
if (opChar > 0 && dca.length() < tf.getMaxSize()) {
out.insert(index++, opChar);
}
return index;
}
/**
* Called by the system to indicate the content has been scrolled
* inside of the form
*
* @param newViewportX the new width of the viewport of the screen
* @param newViewportY the new height of the viewport of the screen
*/
public void uCallScrollChanged(int newViewportX, int newViewportY) {
synchronized (Display.LCDUILock) {
if (hasFocus && inputModeIndicator.getDisplayMode() != null) {
// move input mode indicator because its location depends on
// the item width and item location
moveInputModeIndicator();
}
}
}
/**
* Called by the system to indicate the size available to this Item
* has changed
*
* @param w the new width of the item's content area
* @param h the new height of the item's content area
*/
void uCallSizeChanged(int w, int h) {
super.uCallSizeChanged(w, h);
synchronized (Display.LCDUILock) {
xScrollOffset = 0;
if (textScrollPainter != null) {
stopScroll();
}
startScroll();
// move input mode indicator because its location depends on
// the item width and item location
if (hasFocus && inputModeIndicator.getDisplayMode() != null) {
moveInputModeIndicator();
}
}
}
/**
* Paint the text, scrolling left or right when necessary.
* (A TextField may only be one line hight)
*
* @param g the Graphics to use to paint with. If g is null then
* only the first four arguments are used and nothing is
* painted. Use this to return just the displayed string
* @param dca the text to paint
* @param opChar option char
* @param constraints text constraints
* @param font the font to use to paint the text
* @param fgColor foreground color
* @param w the available width for the text
* @param h the available height for the text
* @param offset the first line pixel offset
* @param options any of Text.[NORMAL | INVERT | HYPERLINK | TRUNCATE]
* @param cursor text cursor object to use to draw vertical bar
*
* @return the current xScrollOffset value which may be changed
* to match the return value of <code>Text.paintLine()</code>
*/
public int paint(Graphics g,
DynamicCharacterArray dca,
char opChar,
int constraints,
Font font,
int fgColor,
int w,
int h,
int offset,
int options,
TextCursor cursor) {
int newXOffset = 0;
g.clipRect(0, 0, w, h);
if (opChar != 0) {
cursor = new TextCursor(cursor);
}
String str = getDisplayString(dca, opChar, constraints,
cursor, true);
if (hasFocus) {
newXOffset = Text.paintLine(g, str, font, fgColor,
w, h, cursor, offset);
} else {
Text.drawTruncString(g, str, font, fgColor, w);
newXOffset = 0;
}
// just correct cursor index if the charracter has
// been already committed
if (str != null && str.length() > 0) {
getBufferString(new DynamicCharacterArray(str),
constraints, cursor, true);
}
showPTPopup((int)0, cursor, w, h);
return newXOffset;
}
// Implementation of Chameleon's TextInputComponent Interface
/**
* Retrieve the initial input mode of this text component as
* defined by the LCDUI TextField API.
*
* @return the initial input mode for this text component or 'null'
* if none was set(?)
*/
public String getInitialInputMode() {
synchronized (Display.LCDUILock) {
return this.initialInputMode;
}
}
/**
* Retrieve the display for the text component
*
* @return the display used for the text component
*/
public Display getDisplay() {
return getCurrentDisplay();
}
/**
* Retrieve the constraints of this text component as defined
* by the LCDUI TextField API.
*
* @return a bitmask which defines the constraints set on this
* text component, or 0 if none were set(?)
*/
public int getConstraints() {
synchronized (Display.LCDUILock) {
return tf.constraints;
}
}
/**
* Returns true if the keyCode is used as 'clear'
* @param keyCode key code
* @return true if keu code is Clear one, false otherwise
*/
public boolean isClearKey(int keyCode) {
return EventConstants.SYSTEM_KEY_CLEAR ==
KeyConverter.getSystemKey(keyCode);
}
/**
* Commit the given input to this TextInputComponent's buffer.
* This call constitutes a change to the value of this TextInputComponent
* and should result in any listeners being notified.
* @param input text to commit
*/
public void commit(String input) {
if (input == null || input.length() == 0) {
return;
}
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
"TF.commit:: " + input);
}
synchronized (Display.LCDUILock) {
try {
cursor.visible = true;
DynamicCharacterArray in = tf.buffer;
TextCursor newCursor = new TextCursor(cursor);
for (int i = 0; i < input.length(); i++) {
String str = getDisplayString(in, input.charAt(i),
tf.constraints,
newCursor, true);
in = new DynamicCharacterArray(str);
}
if (bufferedTheSameAsDisplayed(tf.constraints)) {
if (lValidate(in, tf.constraints)) {
tf.delete(0, tf.buffer.length());
tf.insert(in.toString(), 0);
setCaretPosition(newCursor.index);
tf.notifyStateChanged();
}
} else if (tf.buffer.length() < tf.getMaxSize()) {
tf.insert(input, cursor.index);
tf.notifyStateChanged();
}
} catch (Exception ignore) {
}
}
}
/**
* Set new cursor position
* @param pos new position
*/
protected void setCaretPosition(int pos) {
cursor.index = pos;
if (cursor.index < 0) {
cursor.index = 0;
}
if (cursor.index > tf.buffer.length()) {
cursor.index = tf.buffer.length();
}
}
/**
* Check if the string in the text buffer is the same as the string that
* is actually displayed
*
* @param constraints text input constraints
*
* @return true if the the string is the same otherwise false
*/
protected boolean bufferedTheSameAsDisplayed(int constraints) {
return !((constraints & TextField.PASSWORD) == TextField.PASSWORD
|| (constraints & TextField.CONSTRAINT_MASK) ==
TextField.PHONENUMBER);
}
/**
* Clear the particular number of symbols
*
* @param num number of symbols
*/
public void clear(int num) {
if (cursor.index <= 0) {
return;
}
if (num == 0) {
System.err.println(
"TextFieldLFImpl Warning: asked to delete 0!");
return;
}
if (cursor.index == 0) {
System.err.println(
"TextFieldLFImpl Warning: asked to delete when cursor index is 0!");
return;
}
synchronized (Display.LCDUILock) {
try {
tf.delete(cursor.index - num, num);
} catch (Throwable t) {
t.printStackTrace();
}
tf.notifyStateChanged();
}
}
/**
* This is a notification from the input session that the selected
* input mode has changed. If the TextInputComponent is interested,
* it can query the session for the new InputMode.
*/
public void notifyModeChanged() {
removeInputCommands();
inputModes = inputSession.getAvailableModes();
InputMode im = inputSession.getCurrentInputMode();
inputMenu = new SubMenuCommand(im.getCommandName(), Command.OK, 100);
inputMenu.setListener(this);
addInputCommands();
inputModeIndicator.setDisplayMode(im.getName());
}
// End Chameleon's TextInputComponent Interface
/**
* Called to commit any pending character from the input handler
*/
public void lCommitPendingInteraction() {
// IMPL NOTE: fix needed? inputHandler.endComposition(false);
}
/**
* Handle a pointer press event
*
* @param x pointer x coordinate
* @param y pointer y coordinate
*/
void uCallPointerPressed(int x, int y) {
pressedIn = true;
super.uCallPointerPressed(x, y);
}
/**
* Handle a pointer released event
*
* @param x pointer x coordinate
* @param y pointer y coordinate
*/
void uCallPointerReleased(int x, int y) {
// don't call super method because text field does not have the option
// to activate the command assigned to this item by the pointer
// accept the word if the PTI is currently enabled
if (hasPTI()) {
inputSession.processKey(Constants.KEYCODE_SELECT, false);
}
if (pressedIn) {
int newId = getIndexAt(x, y);
if (newId >= 0 &&
newId <= tf.buffer.length() &&
newId != cursor.index) {
cursor.index = newId;
cursor.option = Text.PAINT_USE_CURSOR_INDEX;
lRequestPaint();
}
pressedIn = false;
}
}
/**
* Get character index at the pointer position
*
* @param x pointer x coordinate
* @param y pointer y coordinate
* @return the character index
*/
protected int getIndexAt(int x, int y) {
int i = -1;
x -= contentBounds[X] +
TextFieldSkin.PAD_H +
xScrollOffset;
if (x >= 0) {
char[] data = tf.buffer.toCharArray();
for (i = 1; i <= tf.buffer.length(); i++) {
if (x <= ScreenSkin.FONT_INPUT_TEXT.charsWidth(data, 0, i)) {
break;
}
}
i--;
}
return i;
}
/**
* Handle a key press
*
* @param keyCode the code for the key which was pressed
*/
void uCallKeyPressed(int keyCode) {
boolean theSameKey = timers.contains(new TimerKey(keyCode));
if (!theSameKey) {
setTimerKey(keyCode);
}
synchronized (Display.LCDUILock) {
// IMPL NOTE: add back in the phone dial support after defining
// more system keys like 'send'
if (KeyConverter.getSystemKey(keyCode) ==
EventConstants.SYSTEM_KEY_SEND) {
if ((getConstraints() & TextField.CONSTRAINT_MASK)
== TextField.PHONENUMBER) {
PhoneDial.call(tf.getString());
}
return;
}
if (!editable) {
// play sound
AlertType.WARNING.playSound(getCurrentDisplay());
return;
}
int key;
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
"TF.processKey keyCode = " + keyCode +
" longPress = " + theSameKey);
}
if ((key = inputSession.processKey(keyCode, theSameKey)) ==
InputMode.KEYCODE_NONE) {
// This means the key wasn't handled by the InputMode
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
"[TF.uCallKeyPressed] returned KEYCODE_NONE");
}
handleClearKey(keyCode, theSameKey);
} else if (key != InputMode.KEYCODE_INVISIBLE) {
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
"[TF.uCallKeyPressed] returned key = " + key);
}
cursor.visible = true;
lRequestPaint();
}
} // synchronized
}
/**
* Handle Clear key
*
* @param keyCode the code for the key which was pressed
* @param longPress true if long key press happens otherwise false
*/
private void handleClearKey(int keyCode, boolean longPress) {
if (isClearKey(keyCode)) {
if (longPress) {
if (tf.buffer.length() > 0) {
tf.delete(0, tf.buffer.length());
tf.notifyStateChanged();
}
} else {
clear(cursor.index -
gePrevCursorIndex(tf.constraints,
tf.buffer,
cursor.index));
}
}
}
/**
* Get previous cursor position
*
* @param constraints text input constraints. The semantics of the
* constraints value are defined in the TextField API.
* @param str text
* @param cursor current cursor index
* @return previious cursor index
*/
private int gePrevCursorIndex(int constraints,
DynamicCharacterArray str,
int cursor) {
cursor--;
if (((constraints & TextField.CONSTRAINT_MASK) ==
TextField.DECIMAL ||
(constraints & TextField.CONSTRAINT_MASK) ==
TextField.NUMERIC) &&
cursor == 1 &&
str.length() == 2 &&
(str.charAt(0) == '-' ||
str.charAt(0) == '.')) {
cursor--;
}
return cursor;
}
/**
* Handle a key release event
*
* @param keyCode key that was released
*
* Obsoleted: Java PhonePad input is replaced by platform input methods.
*
*/
void uCallKeyReleased(int keyCode) {
/*
* IMPL NOTE: abstract the press/release to constants so that
* a port can be configured as to which one to use via the
* constants definition file
*/
cancelTimerKey(keyCode);
}
/**
* Handle a key repeated event
*
* @param keyCode key that was repeated
*/
void uCallKeyRepeated(int keyCode) {
uCallKeyPressed(keyCode);
}
/** Timer to indicate long key press */
class TimerKey extends TimerTask {
/** handling key */
private int key;
/**
* Default constructor for TimerKey
* @param key key code
*/
TimerKey(int key) {
this.key = key;
}
/**
* As soon as timer occures uCallKeyPressed has to be called
* and timer has to be stopped
*/
public final void run() {
uCallKeyPressed(key);
stop();
}
/**
* Start the timer for the pressed key.
* Add this timer to the active pool.
*/
public void start() {
timerService.schedule(this, 700);
timers.addElement(this);
}
/**
* Stop the timer for the pressed key.
* Remove this timer from the active pool.
*/
public void stop() {
cancel();
timers.removeElement(this);
}
/**
* just one timer can be started for the particular key
* @param obj another TimerKey object
* @return true if this TimerKey object equals to another one
*/
public boolean equals(Object obj) {
return ((TimerKey)obj).key == key;
}
}
/** Pool of the active timers */
protected Vector timers = new Vector();
/** The Timer to service TimerTasks. */
protected Timer timerService = new Timer();
/**
* Set a new timer.
*
* @param keyCode the key the timer is started for
*/
protected synchronized void setTimerKey(int keyCode) {
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
"[setTimerKey] for " + keyCode);
}
TimerKey timer = new TimerKey(keyCode);
if (!timers.contains(timer)) {
try {
timer.start();
} catch (IllegalStateException e) {
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
"Exception caught in setTimer");
}
cancelTimerKey(keyCode);
}
}
}
/**
* Cancel any running Timer.
* @param keyCode key the timer is canceled for
*/
protected synchronized void cancelTimerKey(int keyCode) {
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
"[cancelTimerKey] for " + keyCode);
}
TimerKey timer = new TimerKey(keyCode);
int idx = timers.indexOf(timer);
if (idx != -1) {
((TimerKey) timers.elementAt(idx)).stop();
}
}
/**
* Emulate the key click
* @param keyCode key code
*
*/
protected void keyClicked(int keyCode) {
uCallKeyPressed(keyCode);
uCallKeyReleased(keyCode);
}
/**
* Move the text cursor in the given direction
*
* @param dir direction to move
* @return true if the cursor was moved, false otherwise
*/
boolean moveCursor(int dir) {
boolean keyUsed = false;
int newIndex;
switch (dir) {
case Canvas.LEFT:
if (editable) {
keyClicked(dir);
if (cursor.index > 0) {
cursor.option = Text.PAINT_USE_CURSOR_INDEX;
keyUsed = true;
cursor.index--;
}
}
break;
case Canvas.RIGHT:
if (editable) {
keyClicked(dir);
if (cursor.index < tf.buffer.length()) {
cursor.option = Text.PAINT_USE_CURSOR_INDEX;
keyUsed = true;
cursor.index++;
}
}
break;
case Canvas.UP:
case Canvas.DOWN:
default:
break;
}
return keyUsed;
}
/**
* Called by the system
*
* <p>The default implementation of the traverse() method always returns
* false.</p>
*
* @param dir the direction of traversal
* @param viewportWidth the width of the container's viewport
* @param viewportHeight the height of the container's viewport
* @param visRect_inout passes the visible rectangle into the method, and
* returns the unmodified traversal rectangle from this method
* @return true if internal traversal had occurred, false if traversal
* should proceed out
*
* @see #getInteractionModes
* @see #traverseOut
* @see #TRAVERSE_HORIZONTAL
* @see #TRAVERSE_VERTICAL
*/
boolean uCallTraverse(int dir, int viewportWidth, int viewportHeight,
int[] visRect_inout) {
boolean ret = super.uCallTraverse(dir, viewportWidth, viewportHeight,
visRect_inout);
// Show indicator layer
if (showIMPopup) {
if (inputModeIndicator.getDisplayMode() != null) {
getCurrentDisplay().showPopup(inputModeIndicator);
moveInputModeIndicator();
showIMPopup = false;
}
}
return ret;
}
/**
* Called by the system
*
* <p>The default implementation of the traverse() method always returns
* false.</p>
*
* @param dir the direction of traversal
* @param viewportWidth the width of the container's viewport
* @param viewportHeight the height of the container's viewport
* @param visRect_inout passes the visible rectangle into the method, and
* returns the unmodified traversal rectangle from this method
* @return true if internal traversal had occurred, false if traversal
* should proceed out
*
* @see #getInteractionModes
* @see #traverseOut
* @see #TRAVERSE_HORIZONTAL
* @see #TRAVERSE_VERTICAL
*/
boolean lCallTraverse(int dir, int viewportWidth, int viewportHeight,
int[] visRect_inout) {
// By design, traverse should ONLY happen after the item is notified
// to be shown, including the very first show a Form.
// ASSERT (visible == true)
super.lCallTraverse(dir, viewportWidth, viewportHeight, visRect_inout);
boolean ret = false;
Display currentDisplay = getCurrentDisplay();
if (firstTimeInTraverse || dir == CustomItem.NONE) {
if (firstTimeInTraverse) {
if (editable) {
InputMode im = inputSession.getCurrentInputMode();
if (im != null && im.hasDisplayable()) {
enableTF();
} else {
// IMPL NOTE: try-catch has to be removed when the form
// is fixed. Form resets the focus to the 1st item after
// Symbol Table goes away
try {
enableInput();
} catch (Exception ignore) {
}
}
if (interruptedIM != null) {
inputSession.setCurrentInputMode(interruptedIM);
interruptedIM = null;
}
} else {
cursor.option = Text.PAINT_USE_CURSOR_INDEX;
cursor.visible = false;
startScroll();
}
// Show Input Indicator regardless of editability
// so we don't have to turn the layer on/off
// when setConstraints() is called later, which
// may cause deadlock between locks 'layers' and
// LCDUILock.
showIMPopup = true;
firstTimeInTraverse = false;
}
lRequestPaint();
// if (currentDisplay != null) {
// currentDisplay.serviceRepaints(tf.owner.getLF());
// }
ret = true;
} else {
if (moveCursor(dir)) {
lRequestPaint();
// if (currentDisplay != null) {
// currentDisplay.serviceRepaints(tf.owner.getLF());
// }
ret = true;
}
}
// item has to be visible completelly
visRect_inout[X] = 0;
visRect_inout[Y] = 0;
visRect_inout[WIDTH] = bounds[WIDTH];
visRect_inout[HEIGHT] = bounds[HEIGHT];
return ret;
}
/**
* Called by the system to indicate traversal has left this Item
*
* @see #getInteractionModes
* @see #traverse
* @see #TRAVERSE_HORIZONTAL
* @see #TRAVERSE_VERTICAL
*/
void uCallTraverseOut() {
super.uCallTraverseOut();
// Dismiss input mode indicator layer outside LCDUILock
// to avoid deadlocking with Chameleon internal lock 'layers'.
Display currentDisplay = getCurrentDisplay();
if (currentDisplay != null) {
currentDisplay.hidePopup(inputModeIndicator);
hidePTILayer();
}
}
/**
* Called by the system to indicate traversal has left this Item
*
* @see #getInteractionModes
* @see #traverse
* @see #TRAVERSE_HORIZONTAL
* @see #TRAVERSE_VERTICAL
*/
void lCallTraverseOut() {
super.lCallTraverseOut();
firstTimeInTraverse = true;
if (editable) {
InputMode im = inputSession.getCurrentInputMode();
if (im != null && im.hasDisplayable()) {
disableTF();
((ScreenLFImpl)tf.owner.getLF()).resetToTop = false;
} else {
disableInput();
if (im != null) {
interruptedIM = im;
} else {
cursor.index = tf.buffer.length();
}
}
// Leave the hide of indicator layer to uCallTraverseOut
// to avoid deadlocking
} else {
cursor.option = Text.PAINT_USE_CURSOR_INDEX;
cursor.visible = false;
cursor.index = tf.buffer.length();
}
xScrollOffset = 0;
if (textScrollPainter != null) {
stopScroll();
}
lRequestPaint();
}
/**
* Turns the border on or off. hasBorder=false used to indicate
* a textBox, but now that case in handled in the TextBoxLFImpl class
*
* @param state true to turn the border on, false to turn it off
*/
void setBorder(boolean state) {
// API requires this method to exist, so no-op
}
/**
* Move input mode indicator
*/
void moveInputModeIndicator() {
int[] anchor = getInputModeAnchor();
if (inputModeAnchor[0] != anchor[0] ||
inputModeAnchor[1] != anchor[1] ||
inputModeAnchor[2] != anchor[2] ||
inputModeAnchor[3] != anchor[3])
{
inputModeAnchor = anchor;
inputModeIndicator.setAnchor(
inputModeAnchor[0],
inputModeAnchor[1],
inputModeAnchor[2],
inputModeAnchor[3]);
}
}
/**
* This is a utility function to calculate the anchor point
* for the InputModeIndicator layer. This takes into account
* the item's location as well as the containing form's scroll
* location. The array is a 4 element array corresponding to the
* the parameters to the InputModeLayer's setAnchor() method.
* @return input mode anchor (x, y, w, h)
*/
protected int[] getInputModeAnchor() {
int[] anchor = new int[] { 0, 0, 0, 0};
try {
ScreenLFImpl sLF = (ScreenLFImpl)tf.owner.getLF();
int x = getInnerBounds(X) - sLF.viewable[X] + contentBounds[X];
int y = getInnerBounds(Y) - sLF.viewable[Y] + contentBounds[Y];
// anchor x-coordinate relative to InputModeLayer
anchor[0] = x + contentBounds[WIDTH]
+ getCurrentDisplay().getWindow().getBodyAnchorX();
// anchor y-coordinate relative to InputModeLayer
anchor[1] = y + getCurrentDisplay().getWindow().getBodyAnchorY();
// item height
anchor[2] = contentBounds[HEIGHT];
// space below the bottom of the item on the screen
anchor[3] = (sLF.viewport[HEIGHT] + sLF.viewable[Y]) -
(bounds[Y] + bounds[HEIGHT]);
} catch (Throwable t) { }
return anchor;
}
/**
* Initialize or reset variables used to auto-scroll
* uneditable text across this TextField
*/
private void resetUneditable() {
String text = getDisplayString(
tf.buffer, inputSession.getPendingChar(),
tf.constraints, cursor, true);
textWidth = ScreenSkin.FONT_INPUT_TEXT.stringWidth(text);
xScrollOffset = 0;
}
/**
* Start the scrolling of the text in textField
*/
public void startScroll() {
if (editable || !hasFocus || textWidth <= scrollWidth) {
return;
}
stopScroll();
textScrollPainter = new TextScrollPainter();
textScrollTimer.schedule(textScrollPainter, 0,
TextFieldSkin.SCROLL_RATE);
}
/**
* Stop the scrolling of the text in TextField
*/
public void stopScroll() {
if (textScrollPainter == null) {
return;
}
textScrollPainter.cancel();
textScrollPainter = null;
}
/**
* Called repeatedly to animate a side-scroll effect for
* uneditable text within a TextField
*/
public void repaintScrollText() {
if (-xScrollOffset < (textWidth - scrollWidth)) {
xScrollOffset -= TextFieldSkin.SCROLL_SPEED;
lRequestPaint();
} else {
// already scrolled to the end of text
stopScroll();
}
}
/**
* Enable text field
*/
private void enableTF() {
cursor.option = Text.PAINT_USE_CURSOR_INDEX;
cursor.visible = true;
}
/**
* Enable text field input
*/
private void enableInput() {
enableTF();
// ASSERT (editable && hasFocus)
inputSession.beginSession(this);
}
/**
* Disable text field
*/
private void disableTF() {
cursor.option = Text.PAINT_USE_CURSOR_INDEX;
cursor.visible = false;
}
/**
* Disable text field input
*/
private void disableInput() {
disableTF();
removeInputCommands();
inputSession.endSession();
}
/**
* Helper class used to repaint scrolling text
* if needed.
*/
private class TextScrollPainter extends TimerTask {
/**
* Repaint the TextField
*/
public final void run() {
if (!visible) {
stopScroll();
} else {
repaintScrollText();
}
}
}
/**
* Add input modes specific commands
*/
private void addInputCommands() {
inputMenu.removeAll();
if (inputModes != null) {
Command[] inputCommands = new Command[inputModes.length];
for (int i = 0; i < inputModes.length; i++) {
inputCommands[i] = new Command(
inputModes[i].getCommandName(), Command.OK, i);
}
inputMenu.addSubCommands(inputCommands);
// NOTE : adding the command here relies on the
// implementation of Item.addCommand() to not add
// the same command twice, as stated in the MIDP spec
tf.addCommand(inputMenu);
}
}
/**
* Remove input modes specific commands
*/
private void removeInputCommands() {
tf.removeCommand(inputMenu);
}
/**
* Show predictive text popup dialog
* @param keyCode key code
* @param cursor text cursor
* @param width width
* @param height height
*/
protected void showPTPopup(int keyCode, TextCursor cursor,
int width, int height) {
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
"[ showPTPopup] " + keyCode + "," + cursor +
"," + width + "," + height);
}
if (pt_matches.length > 1) { // show layer
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
"[showPTPopup] pt_matches.length =" + pt_matches.length);
}
pt_popup.setList(pt_matches);
showPTILayer();
} else { // hide layer
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
"[hidePTPopup] pt_matches=0");
}
hidePTILayer();
}
}
/**
* Show predictive text popup dialog
*/
protected void showPTILayer() {
Display d = getCurrentDisplay();
if (!pt_popupOpen && d != null) {
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
"[showPTPopup] showing");
}
d.showPopup(pt_popup);
pt_popupOpen = true;
lRequestInvalidate(true, true);
}
}
/**
* Hide predictive text popup dialog
*/
protected void hidePTILayer() {
Display d = getCurrentDisplay();
if (pt_popupOpen && d != null) {
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_HIGHUI,
"[showPTPopup] hiding");
}
d.hidePopup(pt_popup);
pt_popupOpen = false;
lRequestInvalidate(true, true);
}
}
/**
* Check if PTI popup is visible
* @return true if pti layer is visible, false - otherwise
*/
public boolean hasPTI() {
return pt_popupOpen;
}
} // TextFieldLFImpl
|