/*
* Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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 in the LICENSE file that
* accompanied this code).
*
* 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 Oracle, 500 Oracle Parkway, Redwood Shores
* CA 94065 USA or visit www.oracle.com if you need additional information or
* have any questions.
*/
package com.codename1.impl.android;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
import android.text.Editable;
import android.text.InputFilter;
import android.text.InputType;
import android.text.TextWatcher;
import android.text.method.DigitsKeyListener;
import android.text.method.PasswordTransformationMethod;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.TypedValue;
import android.view.ActionMode;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.FrameLayout;
import com.codename1.ui.Component;
import com.codename1.ui.Container;
import com.codename1.ui.Display;
import com.codename1.ui.Font;
import com.codename1.ui.Form;
import com.codename1.ui.TextArea;
import com.codename1.ui.TextField;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.events.ActionListener;
import com.codename1.ui.geom.Dimension;
import com.codename1.ui.plaf.Style;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author lior.gonnen
*
*/
public class InPlaceEditView extends FrameLayout{
private static final String TAG = "InPlaceEditView";
public static final int REASON_UNDEFINED = 0;
public static final int REASON_IME_ACTION = 1;
public static final int REASON_TOUCH_OUTSIDE = 2;
public static final int REASON_SYSTEM_KEY = 3;
static void scrollActiveTextfieldToVisible() {
if (isEditing() && sInstance != null) {
Runnable r = new Runnable() {
@Override
public void run() {
if (sInstance != null && sInstance.mEditText != null && sInstance.mEditText.mTextArea != null) {
TextArea ta = sInstance.mEditText.mTextArea;
if (isScrollableParent(ta)) {
ta.scrollRectToVisible(0, 0, ta.getWidth(), ta.getHeight(), ta);
ta.getComponentForm().getAnimationManager().flushAnimation(new Runnable() {
@Override
public void run() {
reLayoutEdit();
}
});
}
}
}
};
}
}
// The native Android edit-box to place over Codename One's edit-component
private EditView mEditText = null;
private EditView mLastEditText = null;
// The Codename One edit-component we're editing
// The EditText's layout parameters
private FrameLayout.LayoutParams mEditLayoutParams;
// Reference to the system's input method manager
private InputMethodManager mInputManager;
// True while editing is in progress
private static boolean mIsEditing = false;
private static Object editingLock = new Object();
private static boolean waitingForSynchronousEditingCompletion = false;
// Maps Codename One's input-types to Android input-types
private SparseIntArray mInputTypeMap = new SparseIntArray(10);
// Receives results from the InputMethodManager after calling show/hide soft-keyboard methods
private ResultReceiver mResultReceiver;
private int mLastEndEditReason = REASON_UNDEFINED;
private Resources mResources;
// Only a single instance of this class can exist
private static InPlaceEditView sInstance = null;
private static TextArea nextTextArea = null;
private AndroidImplementation impl;
private static long closedTime;
private static boolean showVKB = false;
private static boolean isClosing = false;
// Flag to indicate that the text editor is currently hidden - but an async edit
// is still in progress. This flag is only relevant in async edit mode.
private boolean textEditorHidden = false;
private static boolean resizeMode;
// Used to buffer input while the native editor is being initialized
// This is necessary because initialization may require us to
// asynchronously run code on the EDT to obtain the current text area
// text, and then again asynchronously on the UI thread to set the
// text, and, in the mean time, the user may have typed some text.
private List<TextChange> inputBuffer;
private static Runnable afterClose;
/**
* Private constructor
* To use this class, call the static 'edit' method.
* @param impl The current running activity
*/
private InPlaceEditView(final AndroidImplementation impl) {
super(impl.getActivity());
this.impl = impl;
mResources = impl.getActivity().getResources();
mResultReceiver = new DebugResultReceiver(getHandler());
mInputManager = (InputMethodManager) impl.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
// We place this view as an overlay that takes up the entire screen
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
setFocusableInTouchMode(true);
initInputTypeMap();
setBackgroundDrawable(null);
}
/**
* Prepare an int-to-int map that maps Codename One input-types to
* Android input types
*/
private void initInputTypeMap() {
mInputTypeMap.append(TextArea.ANY, InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
mInputTypeMap.append(TextArea.DECIMAL, InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL | InputType.TYPE_NUMBER_FLAG_SIGNED);
mInputTypeMap.append(TextArea.EMAILADDR, InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
mInputTypeMap.append(TextArea.INITIAL_CAPS_SENTENCE, InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
mInputTypeMap.append(TextArea.INITIAL_CAPS_WORD, InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
mInputTypeMap.append(TextArea.NON_PREDICTIVE, InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
mInputTypeMap.append(TextArea.NUMERIC, InputType.TYPE_CLASS_NUMBER);
mInputTypeMap.append(TextArea.PASSWORD, InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
mInputTypeMap.append(TextArea.PHONENUMBER, InputType.TYPE_CLASS_PHONE);
mInputTypeMap.append(TextArea.URL, InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
}
/**
* Get the Android equivalent input type for a given Codename One input-type
* @param codenameOneInputType One of the com.codename1.ui.TextArea input type constants
* @return The Android equivalent of the given input type
*/
private int getAndroidInputType(int codenameOneInputType) {
int type = mInputTypeMap.get(codenameOneInputType, InputType.TYPE_CLASS_TEXT);
// If we're editing standard text, disable auto complete.
// The name of the flag is a little misleading. From the docs:
// the text editor is performing auto-completion of the text being entered
// based on its own semantics, which it will present to the user as they type.
// This generally means that the input method should not be showing candidates itself,
// but can expect for the editor to supply its own completions/candidates from
// InputMethodSession.displayCompletions().
if ((type & InputType.TYPE_CLASS_TEXT) != 0) {
type |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
}
return type;
}
/**
* Shows the native text editor for the async editing session that is currently in progress.
* This is only used when in async edit mode.
*/
static void showActiveTextEditorAgain() {
if (sInstance != null) {
sInstance.showTextEditorAgain();
}
}
/**
* Allows the implementation to refresh the text field
*/
protected final void repaintTextEditor(final boolean focus) {
Display.getInstance().callSerially(new Runnable() {
public void run() {
if (mEditText != null && mEditText.mTextArea != null) {
mEditText.mTextArea.repaint();
if (focus) {
mEditText.mTextArea.requestFocus();
}
}
}
});
}
/**
* Shows the native text field again after it has been hidden in async edit mode.
*/
private void showTextEditorAgain() {
if (!mIsEditing || !isTextEditorHidden()) {
return;
}
textEditorHidden = false;
final TextArea ta = mEditText.mTextArea;
// Set the input buffer to catch keyboard input occurring between now
// and when we have updated the native editor's text to match the
// current state of the textarea.
// This is necessary in case the textarea's text has been programmatically
// changed since the native aread was hidden.
synchronized (this) {
inputBuffer = new ArrayList<TextChange>();
}
// We are probably not on the EDT. We need to be on the EDT to
// safely get text from the textarea for synchronization.
Display.getInstance().callSerially(new Runnable() {
public void run() {
// Double check that the state is still correct.. i.e. we are editing
// and the editing text area hasn't changed since we issued this call.
if (mIsEditing && mEditText != null && mEditText.mTextArea == ta) {
final String text = ta.getText();
final int cursorPos = ta.getCursorPosition();
// Now that we have our text from the CN1 text area, we need to be on the
// Android UI thread in order to set the text of the native text editor.
impl.getActivity().runOnUiThread(new Runnable() {
public void run() {
// Double check that the state is still correct. I.e. we are editing
// and the editing text area hasn't changed since we issued this call.
if (mIsEditing && mEditText != null && mEditText.mTextArea == ta) {
// We will synchronize here mainly for the benefit of the inputBuffer
// so that we don't find it in an inconsistent state.
synchronized (InPlaceEditView.this) {
// Let's record the cursor positions of the native
// text editor in case we need to use them after synchronizing
// with the CN1 textarea.
int start = cursorPos;
int end = cursorPos;
/*
if (!inputBuffer.isEmpty()) {
// If the input buffer isn't empty, then our start
// and end positions will be "wonky"
start = end = inputBuffer.get(0).atPos;
// If the first change was a delete, then the atPos
// will point to the beginning of the deleted section
// so we need to adjust the end point to be *after*
// the deleted section to begin.
if (inputBuffer.get(0).deleteLength > 0) {
end = start = end + inputBuffer.get(0).deleteLength;
}
}
*/
StringBuilder buf = new StringBuilder();
buf.append(text);
// Loop through any pending changes in the input buffer
// (I.e. key strokes that have occurred since we initiated
// this async callback hell!!)
List<TextChange> tinput = inputBuffer;
if(tinput != null) {
for (TextChange change : tinput) {
// This change is "added" text. Try to add it
// at the correct cursor position. if not, add it at the
// end.
if (change.textToAppend != null) {
if (end >= 0 && end <= buf.length()) {
buf.insert(end, change.textToAppend);
end += change.textToAppend.length();
start = end;
} else {
buf.append(change.textToAppend);
end = buf.length();
start = end;
}
}
// The change is "deleted" text.
else if (change.deleteLength > 0) {
if (end >= change.deleteLength && end <= buf.length()) {
buf.delete(end - change.deleteLength, end);
end -= change.deleteLength;
start = end;
} else if (end > 0 && end < change.deleteLength) {
buf.delete(0, end);
end = 0;
start = end;
}
}
}
}
// Important: Clear the input buffer so that the TextWatcher
// knows to stop filling it up. We only need the inputBuffer
// to keep input between the original showTextEditorAgain() call
// and here.
inputBuffer = null;
mEditText.setText(buf.toString());
if (start < 0 || start > mEditText.getText().length()) {
start = mEditText.getText().length();
}
if (end < 0 || end > mEditText.getText().length()) {
end = mEditText.getText().length();
}
// Update the caret in the edit text field so we can continue.
mEditText.setSelection(start, end);
}
}
}
});
}
}
});
reLayoutEdit(true);
repaintTextEditor(true);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
endEdit(true);
}
/**
* Hides the native text editor while keeping the active async edit session going.
* This will effectively hide the native text editor, and show the light-weight text area
* with cursor still in the correct position.
*
* <p>This is just a static wrapper around {@link #hideTextEditor()}</p>
*/
static void hideActiveTextEditor() {
if (sInstance != null) {
sInstance.hideTextEditor();
}
}
/**
* Hides the native text editor while keeping the active async edit session going.
* This will effectively hide the native text editor, and show the light-weight text area
* with cursor still in the correct position.
*/
private void hideTextEditor() {
if (!mIsEditing || textEditorHidden || mEditText == null) {
return;
}
textEditorHidden = true;
final TextArea ta = mEditText.mTextArea;
// Since this may be called off the UI thread, we need to issue async request on UI thread
// to hide the text area.
impl.getActivity().runOnUiThread(new Runnable() {
public void run() {
if (mEditText != null && mEditText.mTextArea == ta) {
// Note: Setting visibility to GONE doesn't work here because the TextWatcher
// will stop receiving input from the keyboard, so we don't have a way to
// reactivate the text editor when the user starts typing again. Using the margin
// to move it off screen keeps the text editor active.
mEditLayoutParams.setMargins(-Display.getInstance().getDisplayWidth(), 0, 0, 0);
InPlaceEditView.this.requestLayout();
final int cursorPos = mEditText.getSelectionStart();
// Since we are going to be displaying the CN1 text area now, we need to update
// the cursor. That needs to happen on the EDT.
Display.getInstance().callSerially(new Runnable() {
public void run() {
if (mEditText != null && mEditText.mTextArea == ta && mIsEditing && textEditorHidden) {
if (ta instanceof TextField) {
((TextField)ta).setCursorPosition(cursorPos);
}
}
}
});
}
}
});
// Repaint the CN1 text area on the EDT. This is necessary because while the native editor
// was shown, the cn1 text area paints only its background. Now that the editor is hidden
// it should paint its foreground also.
Display.getInstance().callSerially(new Runnable() {
public void run() {
if (mEditText != null && mEditText.mTextArea != null) {
mEditText.mTextArea.repaint();
}
}
});
//repaintTextEditor(true);
}
/**
* Checks if the native text editor is currently hidden. Only relevant in async edit mode.
*
* <p>This is just a static wrapper around {@link #isTextEditorHidden()}</p>
* @return
*/
static boolean isActiveTextEditorHidden() {
if (sInstance != null) {
return sInstance.isTextEditorHidden();
}
return true;
}
/**
* Checks if the native text editor is currently hidden. Only relevant in async edit mode.
* @return
*/
private boolean isTextEditorHidden() {
return textEditorHidden;
}
/*
static void handleActiveTouchEventIfHidden(MotionEvent event) {
if (sInstance != null && mIsEditing && isActiveTextEditorHidden()) {
sInstance.onTouchEvent(event);
}
}
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!impl.isAsyncEditMode()) {
boolean leaveVKBOpen = false;
if (mEditText != null && mEditText.mTextArea != null && mEditText.mTextArea.getComponentForm() != null) {
Component c = mEditText.mTextArea.getComponentForm().getComponentAt((int) event.getX(), (int) event.getY());
if ( mEditText.mTextArea.getClientProperty("leaveVKBOpen") != null
|| (c != null && c instanceof TextArea && ((TextArea) c).isEditable() && ((TextArea) c).isEnabled())) {
leaveVKBOpen = true;
}
}
// When the user touches the screen outside the text-area, finish editing
endEditing(REASON_TOUCH_OUTSIDE, leaveVKBOpen);
} else {
final int evtX = (int) event.getX();
final int evtY = (int) event.getY();
Display.getInstance().callSerially(new Runnable() {
public void run() {
if (mEditText != null && mEditText.mTextArea != null) {
TextArea tx = mEditText.mTextArea;
int x = tx.getAbsoluteX() + tx.getScrollX();
int y = tx.getAbsoluteY() + tx.getScrollY();
int w = tx.getWidth();
int h = tx.getHeight();
if (!(x <= evtX && y <= evtY && x + w >= evtX && y + h >= evtY)) {
hideTextEditor();
} else {
showTextEditorAgain();
}
}
}
});
}
// Return false so that the event will propagate to the underlying view
// We don't want to consume this event
return false;
}
/**
* Show or hide the virtual keyboard if necessary
* @param show Show the keyboard if true, hide it otherwise
*/
private void showVirtualKeyboard(boolean show) {
Log.i(TAG, "showVirtualKeyboard show=" + show);
boolean result = false;
if (show) {
// If we're in landscape, Android will not show the soft
// keyboard unless SHOW_FORCED is requested
Configuration config = mResources.getConfiguration();
boolean isLandscape = (config.orientation == Configuration.ORIENTATION_LANDSCAPE);
int showFlags = isLandscape ? InputMethodManager.SHOW_FORCED : InputMethodManager.SHOW_IMPLICIT;
mInputManager.restartInput(mEditText);
result = mInputManager.showSoftInput(mEditText, showFlags, mResultReceiver);
} else {
if(mEditText == null){
if(showVKB){
mInputManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
}
}else{
result = mInputManager.hideSoftInputFromWindow(mEditText.getWindowToken(), 0, mResultReceiver);
}
closedTime = System.currentTimeMillis();
}
showVKB = show;
final boolean showKeyboard = showVKB;
final ActionListener listener = Display.getInstance().getVirtualKeyboardListener();
if(listener != null){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
//this is ugly but there is no real API to know if the
//keyboard is opened or closed
try {
Thread.sleep(600);
} catch (InterruptedException ex) {
}
Display.getInstance().callSerially(new Runnable() {
@Override
public void run() {
ActionEvent evt = new ActionEvent(showKeyboard);
listener.actionPerformed(evt);
}
});
}
});
t.setUncaughtExceptionHandler(AndroidImplementation.exceptionHandler);
t.start();
}
Log.d(TAG, "InputMethodManager returned " + Boolean.toString(result).toUpperCase());
}
/**
* Returns true if the keyboard is currently on screen.
*/
public static boolean isKeyboardShowing(){
//There is no android API to know if the keyboard is currently showing
//This method will return false after 2 seconds since the keyboard was
//requested to be closed
return showVKB || (System.currentTimeMillis() - closedTime) < 2000;
}
static class TextAreaData {
final int absoluteY;
final int absoluteX;
final int paddingTop;
final int paddingLeft;
final int paddingRight;
final int paddingBottom;
final int scrollX;
final int scrollY;
final int verticalAlignment;
final int height;
final int width;
final int fontHeight;
final TextArea textArea;
final Component nextDown;
final boolean isRTL;
final boolean isSingleLineTextArea;
final String hint;
final boolean nativeHintBool;
final Object nativeFont;
final int fgColor;
final int maxSize;
final boolean isTextField;
int getAbsoluteY() {
return absoluteY;
}
int getAbsoluteX() {
return absoluteX;
}
int getScrollX() {
return scrollX;
}
int getScrollY() {
return scrollY;
}
int getHeight() {
return height;
}
int getWidth() {
return width;
}
int getVerticalAlignment() {
return verticalAlignment;
}
boolean isRTL() {
return isRTL;
}
boolean isSingleLineTextArea() {
return isSingleLineTextArea;
}
Object getClientProperty(String key) {
return textArea.getClientProperty(key);
}
void putClientProperty(String key, Object value) {
textArea.putClientProperty(key, value);
}
Object getDoneListener() {
if (isTextField) {
return ((TextField)textArea).getDoneListener();
}
return null;
}
String getHint() {
return hint;
}
TextAreaData(TextArea ta) {
absoluteX = ta.getAbsoluteX();
absoluteY = ta.getAbsoluteY();
scrollX = ta.getScrollX();
scrollY = ta.getScrollY();
Style s = ta.getStyle();
paddingTop = s.getPaddingTop();
paddingLeft = s.getPaddingLeft(ta.isRTL());
paddingRight = s.getPaddingRight(ta.isRTL());
paddingBottom = s.getPaddingBottom();
isTextField = (ta instanceof TextField);
verticalAlignment = ta.getVerticalAlignment();
height = ta.getHeight();
width = ta.getWidth();
fontHeight = s.getFont().getHeight();
textArea = ta;
isRTL = ta.isRTL();
nextDown = textArea.getNextFocusDown() != null ? textArea.getNextFocusDown() : textArea.getComponentForm().findNextFocusVertical(true);
isSingleLineTextArea = textArea.isSingleLineTextArea();
hint = ta.getHint();
nativeHintBool = textArea.getUIManager().isThemeConstant("nativeHintBool", false);
nativeFont = s.getFont().getNativeFont();
fgColor = s.getFgColor();
maxSize = ta.getMaxSize();
}
}
// Timers for manually blinking cursor on Android 4.4
private Timer cursorTimer;
private TimerTask cursorTimerTask;
/**
* Start editing the given text-area
* This method is executed on the UI thread, so UI manipulation is safe here.
* @param activity Current running activity
* @param textArea The TextAreaData instance that wraps the CN1 TextArea that our internal EditText needs to overlap. We use
* a TextAreaData so that the text area properties can be accessed off the EDT safely.
* @param codenameOneInputType One of the input type constants in com.codename1.ui.TextArea
* @param initialText The text that appears in the Codename One text are before the call to startEditing
* @param isEditedFieldSwitch if true, then special case for async edit mode - the native editing is already active, no need to show
* native field, just change the connected field
*/
private synchronized void startEditing(Activity activity, TextAreaData textArea, String initialText, int codenameOneInputType, final boolean isEditedFieldSwitch) {
int txty = lastTextAreaY = textArea.getAbsoluteY() + textArea.getScrollY();
int txtx = lastTextAreaX = textArea.getAbsoluteX() + textArea.getScrollX();
lastTextAreaWidth = textArea.getWidth();
lastTextAreaHeight = textArea.getHeight();
int paddingTop = 0;
int paddingLeft = textArea.paddingLeft;
int paddingRight = textArea.paddingRight;
int paddingBottom = textArea.paddingBottom;
if (textArea.isTextField) {
switch (textArea.getVerticalAlignment()) {
case Component.BOTTOM:
paddingTop = textArea.getHeight() - textArea.paddingBottom - textArea.fontHeight;
break;
case Component.CENTER:
paddingTop = textArea.getHeight() / 2 - textArea.fontHeight / 2;
break;
default:
paddingTop = textArea.paddingTop;
break;
}
} else {
paddingTop = textArea.paddingTop;
}
int id = activity.getResources().getIdentifier("cn1Style", "attr", activity.getApplicationInfo().packageName);
if (!isEditedFieldSwitch) {
mEditText = new EditView(activity, textArea.textArea, this, id);
} else {
mEditText.switchToTextArea(textArea.textArea);
}
if(textArea.getClientProperty("blockCopyPaste") != null || Display.getInstance().getProperty("blockCopyPaste", "false").equals("true")) {
// The code below is taken from this stackoverflow answer: http://stackoverflow.com/a/22756538/756809
if (android.os.Build.VERSION.SDK_INT < 11) {
mEditText.setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
menu.clear();
}
});
} else {
mEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
public void onDestroyActionMode(ActionMode mode) {
}
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return false;
}
public boolean onActionItemClicked(ActionMode mode,
MenuItem item) {
return false;
}
});
}
}
else if (isEditedFieldSwitch) {
//reset copy-paste protection
if (android.os.Build.VERSION.SDK_INT < 11) {
mEditText.setOnCreateContextMenuListener(null);
} else {
mEditText.setCustomSelectionActionModeCallback(null);
}
}
if (!isEditedFieldSwitch) {
mEditText.addTextChangedListener(mEditText.mTextWatcher);
}
mEditText.setBackgroundDrawable(null);
mEditText.setFocusableInTouchMode(true);
mEditLayoutParams = new FrameLayout.LayoutParams(0, 0);
// Set the appropriate gravity so that the left and top margins will be
// taken into account
mEditLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
mEditLayoutParams.setMargins(txtx, txty, 0, 0);
mEditLayoutParams.width = textArea.getWidth();
mEditLayoutParams.height = textArea.getHeight();
mEditText.setLayoutParams(mEditLayoutParams);
if(textArea.isRTL()){
mEditText.setGravity(Gravity.RIGHT | Gravity.TOP);
}else{
mEditText.setGravity(Gravity.LEFT | Gravity.TOP);
}
mEditText.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
Component nextDown = textArea.nextDown;
boolean imeOptionTaken = true;
int ime = EditorInfo.IME_FLAG_NO_EXTRACT_UI;
if (textArea.isSingleLineTextArea()) {
if(textArea.getClientProperty("searchField") != null) {
mEditText.setImeOptions(ime | EditorInfo.IME_ACTION_SEARCH);
} else {
if(textArea.getClientProperty("sendButton") != null) {
mEditText.setImeOptions(ime | EditorInfo.IME_ACTION_SEND);
} else {
if(textArea.getClientProperty("goButton") != null) {
mEditText.setImeOptions(ime | EditorInfo.IME_ACTION_GO);
} else {
if(textArea.isTextField && textArea.getDoneListener() != null){
mEditText.setImeOptions(ime | EditorInfo.IME_ACTION_DONE);
} else if (nextDown != null && nextDown instanceof TextArea && ((TextArea)nextDown).isEditable() && ((TextArea)nextDown).isEnabled()) {
mEditText.setImeOptions(ime | EditorInfo.IME_ACTION_NEXT);
} else {
mEditText.setImeOptions(ime | EditorInfo.IME_ACTION_DONE);
imeOptionTaken = false;
}
}
}
}
}
mEditText.setSingleLine(textArea.isSingleLineTextArea());
mEditText.setAdapter((ArrayAdapter<String>) null);
mEditText.setText(initialText);
if(!textArea.isSingleLineTextArea() && textArea.textArea.isGrowByContent() && textArea.textArea.getGrowLimit() > -1){
mEditText.setMaxLines(textArea.textArea.getGrowLimit());
}
if(textArea.nativeHintBool && textArea.getHint() != null) {
mEditText.setHint(textArea.getHint());
}
if (!isEditedFieldSwitch) {
addView(mEditText, mEditLayoutParams);
}
invalidate();
setVisibility(VISIBLE);
bringToFront();
mEditText.requestFocus();
Object nativeFont = textArea.nativeFont;
if (nativeFont == null) {
nativeFont = impl.getDefaultFont();
}
Paint p = (Paint) ((AndroidImplementation.NativeFont) nativeFont).font;
mEditText.setTypeface(p.getTypeface());
mEditText.setTextScaleX(p.getTextScaleX());
mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, p.getTextSize());
int fgColor = textArea.fgColor;
mEditText.setTextColor(Color.rgb(fgColor >> 16, (fgColor & 0x00ff00) >> 8, (fgColor & 0x0000ff)));
boolean password = false;
if((codenameOneInputType & TextArea.PASSWORD) == TextArea.PASSWORD){
codenameOneInputType = codenameOneInputType ^ TextArea.PASSWORD;
password = true;
}
if (textArea.isSingleLineTextArea()) {
mEditText.setInputType(getAndroidInputType(codenameOneInputType));
//if not ime was explicity requested and this is a single line textfield of type ANY add the emoji keyboard.
if(!imeOptionTaken && codenameOneInputType == TextArea.ANY){
mEditText.setInputType(getAndroidInputType(codenameOneInputType) | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE);
}
if(Display.getInstance().getProperty("andAddComma", "false").equals("true") &&
(codenameOneInputType & TextArea.DECIMAL) == TextArea.DECIMAL) {
mEditText.setKeyListener(DigitsKeyListener.getInstance("0123456789.,"));
}
}
if (password) {
int type = mInputTypeMap.get(codenameOneInputType, InputType.TYPE_CLASS_TEXT);
if((type & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) == InputType.TYPE_TEXT_FLAG_CAP_SENTENCES){
type = type ^ InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
}
//turn off suggestions for passwords
mEditText.setInputType(type | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
mEditText.setTransformationMethod(new MyPasswordTransformationMethod());
}
int maxLength = textArea.maxSize;
InputFilter[] FilterArray = new InputFilter[1];
FilterArray[0] = new InputFilter.LengthFilter(maxLength);
mEditText.setFilters(FilterArray);
mEditText.setSelection(mEditText.getText().length());
showVirtualKeyboard(true);
if (Build.VERSION.SDK_INT < 21) {
// HACK!!! On Android 4.4, it seems that the natural blinking cursor
// causes text to disappear when it blinks. Manually blinking the
// cursor seems to work around this issue, so that's what we do here.
// This issue is described here: http://stackoverflow.com/questions/41305052/textfields-content-disappears-during-typing?noredirect=1#comment69977316_41305052
mEditText.setCursorVisible(false);
final boolean[] cursorVisible = new boolean[]{false};
if (cursorTimer != null) {
cursorTimer.cancel();
}
cursorTimer = new Timer();
cursorTimerTask = new TimerTask() {
public void run() {
AndroidNativeUtil.getActivity().runOnUiThread(new Runnable() {
public void run() {
EditView v = mEditText;
if (v != null) {
cursorVisible[0] = !cursorVisible[0];
v.setCursorVisible(cursorVisible[0]);
}
}
});
}
};
cursorTimer.schedule(cursorTimerTask, 100, 500);
}
}
/**
* Calculate the font height in pixels according to the text area
* @param style The Codename One text-area to get the font height from
* @return The font height in pixels, or -1 if a font height could not be determined.
*/
private int getFontHeight(Style style) {
if (style == null) {
return -1;
}
Font font = style.getFont();
if (font == null) {
return -1;
}
return font.getHeight();
}
static boolean activeEditorContains(int x, int y) {
return sInstance != null && sInstance.editorContains(x, y);
}
private boolean editorContains(int x, int y) {
return mIsEditing && mEditText != null && mEditText.mTextArea != null && mEditText.mTextArea.contains(x, y);
}
private synchronized void endEditing(int reason, boolean forceVKBOpen) {
endEditing(reason, forceVKBOpen, false);
}
/**
* Finish the in-place editing of the given text area, release the edit lock, and allow the synchronous call
* to 'edit' to return.
*/
private synchronized void endEditing(int reason, boolean forceVKBOpen, boolean forceVKBClose) {
if (cursorTimer != null) {
cursorTimer.cancel();
}
if (!mIsEditing || mEditText == null) {
return;
}
// SJH: Setting visibility GONE causes a size change event to be fired even when the
// input mode is adjustPan. This causes problems and glitches with the layout because we
// have to guess if a resize even is accurate or not.
//setVisibility(GONE);
mLastEndEditReason = reason;
// If the IME action is set to NEXT, do not hide the virtual keyboard
boolean isNextActionFlagSet = ((mEditText.getImeOptions() & 0xf) == EditorInfo.IME_ACTION_NEXT);
boolean leaveKeyboardShowing = impl.isAsyncEditMode() || (reason == REASON_IME_ACTION) && isNextActionFlagSet || forceVKBOpen;
if (forceVKBClose) {
leaveKeyboardShowing = false;
}
if (!leaveKeyboardShowing) {
showVirtualKeyboard(false);
}
int imo = mEditText.getImeOptions() & 0xf; // Get rid of flags
if (reason == REASON_IME_ACTION
&& mEditText.mTextArea instanceof TextField
&& ((TextField) mEditText.mTextArea).getDoneListener() != null
&& ((imo & EditorInfo.IME_ACTION_DONE) != 0 || (imo & EditorInfo.IME_ACTION_SEARCH) != 0 || (imo & EditorInfo.IME_ACTION_SEND) != 0 || (imo & EditorInfo.IME_ACTION_GO) != 0)) {
((TextField) mEditText.mTextArea).fireDoneEvent();
showVirtualKeyboard(false);
}
// Call this in onComplete instead
//mIsEditing = false;
mLastEditText = mEditText;
removeView(mEditText);
Component editingComponent = mEditText.mTextArea;
mEditText.removeTextChangedListener(mEditText.mTextWatcher);
mEditText = null;
if (impl.isAsyncEditMode()) {
Runnable onComplete = (Runnable)editingComponent.getClientProperty("android.onAsyncEditingComplete");
editingComponent.putClientProperty("android.onAsyncEditingComplete", null);
if (onComplete != null) {
Display.getInstance().callSerially(onComplete);
}
}
waitingForSynchronousEditingCompletion = false;
}
/**
* This method waits until the user leaves the EditText
* It must not access sInstance since it might not have been created yet.
*/
private static void waitForEditCompletion() {
Display.getInstance().invokeAndBlock(new Runnable() {
public void run() {
while (waitingForSynchronousEditingCompletion){
try {
Thread.sleep(50);
} catch (Throwable e) {
}
};
}
});
Log.d(TAG, "waitForEditCompletion - Waiting for lock");
}
/**
* This method will be called by our EditText control when the action
* key (Enter/Go/Send) on the soft keyboard will be pressed.
* @param actionCode
* @return task to run after call to super.onEditorAction. Returns null if action was consumed (tapped Next in async mode) and super.onEditorAction do not have to be called.
*/
Runnable onEditorAction(int actionCode) {
actionCode = actionCode & 0xf;
boolean hasNext = false;
if (EditorInfo.IME_ACTION_NEXT == actionCode && mEditText != null &&
mEditText.mTextArea != null) {
Component next = mEditText.mTextArea.getNextFocusDown();
if (next == null) {
next = mEditText.mTextArea.getComponentForm().findNextFocusVertical(true);
}
if (next != null && next instanceof TextArea && ((TextArea)next).isEditable() && ((TextArea)next).isEnabled()) {
hasNext = true;
nextTextArea = (TextArea) next;
}
}
if (hasNext && nextTextArea != null && impl.isAsyncEditMode()) {
//in async edit mode go right to next field edit to avoid hiding and showing again the native edit text
TextArea theNext = nextTextArea;
nextTextArea = null;
edit(sInstance.impl, theNext, theNext.getConstraint());
return null;
} else {
return new Runnable() {
@Override
public void run() {
endEditing(REASON_IME_ACTION, false);
}
};
}
}
private static int trySetEditModeCount=0;
private static void trySetEditMode(final boolean resize) {
trySetEditMode(resize, 10);
}
private static com.codename1.ui.geom.Rectangle getVisibleRect(Component c) {
com.codename1.ui.geom.Rectangle r = new com.codename1.ui.geom.Rectangle(c.getAbsoluteX() + c.getScrollX(), c.getAbsoluteY() + c.getScrollY(), c.getWidth(), c.getHeight());
while ((c = c.getParent()) != null) {
com.codename1.ui.geom.Rectangle.intersection(r.getX(), r.getY(), r.getWidth(), r.getHeight(),
c.getAbsoluteX() + c.getScrollX(), c.getAbsoluteY() + c.getScrollY(), c.getWidth(), c.getHeight(),
r);
}
return r;
}
/**
* Wrap the setEditMode method so that it is "safe" to set to resize edit mode.
* This will try to set to the "resize" edit mode, and will spawn a thread
* to check 200 ms later to make sure that the text area is not covered by the
* keyboard. If it is covered by the keyboard, it will switch to pan edit mode.
* This is preferable since resize edit mode works better in general so we'd like
* to use resize whenever possible.
*
* This change is to minimize the number of occurrences of this bug:
* https://github.com/codenameone/CodenameOne/issues/1827
*
* Some attempts were made to fix this bug directly by wrapping the textarea
* inside a ScrollView, but it caused some issues when the user tries to make text selections
* on the textview content - which causes Android to implicitly *change* to resize edit mode
* without actually letting us know. This gist shows that attempt:
* https://gist.github.com/shannah/471033878e53c1fc297680fa85f6fd20
*
*
* @param resize
*/
private static void trySetEditMode(final boolean resize, final int retriesRemaining) {
if (trySetEditModeCount > 100) {
trySetEditModeCount = 0;
}
final int thisCount = trySetEditModeCount++;
if (resize != resizeMode) {
setEditMode(resize);
}
if (resize) {
// We would like to set pan mode, but we must do this with some protections
// since pan mode might cover the text area
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(100);
} catch (Exception ex){}
if (thisCount != trySetEditModeCount-1) {
return;
}
AndroidNativeUtil.getActivity().runOnUiThread(new Runnable() {
public void run() {
if (thisCount != trySetEditModeCount-1) {
return;
}
if (sInstance == null || sInstance.mEditText == null || sInstance.mEditText.mTextArea == null) {
return;
}
com.codename1.ui.Font font = sInstance.mEditText.mTextArea.getStyle().getFont();
float fontSize = (font != null || font.getPixelSize() == 0) ? font.getPixelSize() : Display.getInstance().convertToPixels(4);
com.codename1.ui.geom.Rectangle visibleRect = getVisibleRect(sInstance.mEditText.mTextArea);
Rect r = new Rect();
AndroidImplementation.getInstance().relativeLayout.getGlobalVisibleRect(r);
int rootViewHeight = r.height();
int txtY = sInstance.mEditText.mTextArea.getAbsoluteY() + sInstance.mEditText.mTextArea.getScrollY();
if (txtY > rootViewHeight - 20 || visibleRect.getHeight() < fontSize) {
// We're off the page
//System.out.println("SETTING TO PAN MODE_______");
setEditMode(false);
} else {
if (retriesRemaining > 0) {
trySetEditMode(resize, retriesRemaining-1);
}
}
}
});
}
}).start();
}
}
private static void setEditMode(final boolean resize){
resizeMode = resize;
if (resize) {
sInstance.impl.getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
} else {
sInstance.impl.getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
}
}
public static boolean isInputResize(){
return resizeMode;
}
/**
* Returns true if an edit is currently in progress, false otherwise
*/
public static boolean isEditing() {
return (sInstance == null) ? false : sInstance.mIsEditing;
}
public static int lastEditEndReason() {
return (sInstance == null) ? REASON_UNDEFINED : sInstance.mLastEndEditReason;
}
public static void endEdit() {
endEdit(false);
}
// Called on Android UI thread.
public static void endEdit(boolean forceVKBClose) {
if (sInstance != null) {
sInstance.endEditing(REASON_UNDEFINED, false, forceVKBClose);
// No longer need these because end editing will allow the onComplete handler to
// be called which will trigger a releaseEdit
//ViewParent p = sInstance.getParent();
//if (p != null) {
// ((ViewGroup) p).removeView(sInstance);
//}
//sInstance = null;
}
}
public static void stopEdit() {
stopEdit(false);
}
public static void stopEdit(boolean forceVKBClose) {
if (sInstance != null) {
sInstance.endEditing(REASON_UNDEFINED, false, forceVKBClose);
}
}
private static void releaseEdit() {
if (sInstance != null) {
ViewParent p = sInstance.getParent();
if (p != null) {
((ViewGroup) p).removeView(sInstance);
}
sInstance = null;
}
}
private static int lastTextAreaX, lastTextAreaY, lastTextAreaWidth, lastTextAreaHeight;
/*
public static void reLayoutEdit() {
if (sInstance != null && sInstance.mEditText != null) {
TextArea txt = sInstance.mEditText.mTextArea;
if (txt != null) {
int txty = txt.getAbsoluteY() + txt.getScrollY();
int txtx = txt.getAbsoluteX() + txt.getScrollX();
int w = txt.getWidth();
int h = txt.getHeight();
sInstance.mEditLayoutParams.setMargins(txtx, txty, 0, 0);
sInstance.mEditLayoutParams.width = w;
sInstance.mEditLayoutParams.height = h;
sInstance.mEditText.requestLayout();
sInstance.invalidate();
sInstance.setVisibility(VISIBLE);
sInstance.bringToFront();
}
}
}*/
public static void reLayoutEdit() {
reLayoutEdit(false);
}
public static void reLayoutEdit(boolean force) {
if (mIsEditing && !isActiveTextEditorHidden() && sInstance != null && sInstance.mEditText != null) {
final TextArea txt = sInstance.mEditText.mTextArea;
if (!force && lastTextAreaX == txt.getAbsoluteX() + txt.getScrollX() &&
lastTextAreaY == txt.getAbsoluteY() + txt.getScrollY() &&
lastTextAreaWidth == txt.getWidth() &&
lastTextAreaHeight == txt.getHeight()) {
return;
}
Display.getInstance().callSerially(new Runnable() {
public void run() {
if (mIsEditing && !isActiveTextEditorHidden() && sInstance != null && sInstance.mEditText != null) {
if (txt != null) {
if (sInstance.mEditText.mTextArea != txt) {
//has changed in between, skip or would change location back to old field
return;
}
final int txty = lastTextAreaY = txt.getAbsoluteY() + txt.getScrollY();
final int txtx = lastTextAreaX = txt.getAbsoluteX() + txt.getScrollX();
final int w = lastTextAreaWidth = txt.getWidth();
final int h = lastTextAreaHeight = txt.getHeight();
sInstance.impl.getActivity().runOnUiThread(new Runnable() {
public void run() {
if (mIsEditing && !isActiveTextEditorHidden() && sInstance != null && sInstance.mEditText != null) {
if (sInstance.mEditText.mTextArea != txt) {
//has changed in between, skip or would change location back to old field
return;
}
sInstance.mEditLayoutParams.setMargins(txtx, txty, 0, 0);
sInstance.mEditLayoutParams.width = w;
sInstance.mEditLayoutParams.height = h;
sInstance.mEditText.requestLayout();
sInstance.invalidate();
sInstance.setVisibility(VISIBLE);
sInstance.bringToFront();
}
}
});
}
}
}
});
}
}
/**
* Entry point for using this class
* @param impl The current running activity
* @param component Any subclass of com.codename1.ui.TextArea
* @param inputType One of the TextArea's input-type constants
*/
public static void edit(final AndroidImplementation impl, final Component component, final int inputType) {
if (impl.getActivity() == null) {
throw new IllegalArgumentException("activity is null");
}
if (component == null) {
throw new IllegalArgumentException("component is null");
}
if (!(component instanceof TextArea)) {
throw new IllegalArgumentException("component must be instance of TextArea");
}
final TextArea textArea = (TextArea) component;
final String initialText = textArea.getText();
textArea.putClientProperty("InPlaceEditView.initialText", initialText);
Dimension prefSize = textArea.getPreferredSize();
// The very first time we try to edit a string, let's determine if the
// system default is to do async editing. If the system default
// is not yet set, we set it here, and it will be used as the default from now on
// We do this because the nativeInstance.isAsyncEditMode() value changes
// to reflect the currently edited field so it isn't a good way to keep a
// system default.
String defaultAsyncEditingSetting = Display.getInstance().getProperty("android.VKBAlwaysOpen", null);
if (defaultAsyncEditingSetting == null) {
defaultAsyncEditingSetting = impl.isAsyncEditMode() ? "true" : "false";
Display.getInstance().setProperty("android.VKBAlwaysOpen", defaultAsyncEditingSetting);
}
boolean asyncEdit = "true".equals(defaultAsyncEditingSetting) ? true : false;
// Check if the form has any setting for asyncEditing that should override
// the application defaults.
final Form parentForm = component.getComponentForm();
if (parentForm == null) {
com.codename1.io.Log.p("Attempt to edit text area that is not on a form. This is not supported");
return;
}
if (parentForm.getClientProperty("asyncEditing") != null) {
Object async = parentForm.getClientProperty("asyncEditing");
if (async instanceof Boolean) {
asyncEdit = ((Boolean)async).booleanValue();
//Log.p("Form overriding asyncEdit due to asyncEditing client property: "+asyncEdit);
}
}
if (parentForm.getClientProperty("android.asyncEditing") != null) {
Object async = parentForm.getClientProperty("android.asyncEditing");
if (async instanceof Boolean) {
asyncEdit = ((Boolean)async).booleanValue();
//Log.p("Form overriding asyncEdit due to ios.asyncEditing client property: "+asyncEdit);
}
}
if (parentForm.isFormBottomPaddingEditingMode()) {
asyncEdit = true;
}
// If the field itself explicitly sets async editing behaviour
// then this will override all other settings.
if (component.getClientProperty("asyncEditing") != null) {
Object async = component.getClientProperty("asyncEditing");
if (async instanceof Boolean) {
asyncEdit = ((Boolean)async).booleanValue();
//Log.p("Overriding asyncEdit due to field asyncEditing client property: "+asyncEdit);
}
}
if (component.getClientProperty("android.asyncEditing") != null) {
Object async = component.getClientProperty("android.asyncEditing");
if (async instanceof Boolean) {
asyncEdit = ((Boolean)async).booleanValue();
//Log.p("Overriding asyncEdit due to field ios.asyncEditing client property: "+asyncEdit);
}
}
//if true, then in async mode we are currently editing and are switching to another field
final boolean isEditedFieldSwitch;
// If we are already editing, we need to finish that up before we proceed to edit the next field.
synchronized(editingLock) {
if (mIsEditing) {
if (impl.isAsyncEditMode()) {
isEditedFieldSwitch = true;
final String[] out = new String[1];
TextArea prevTextArea = null;
if(sInstance != null && sInstance.mLastEditText != null) {
prevTextArea = sInstance.mLastEditText.getTextArea();
}
if (prevTextArea != null) {
final TextArea fPrevTextArea = prevTextArea;
final String retVal = sInstance.mLastEditText.getText().toString();
Display.getInstance().callSerially(new Runnable() {
public void run() {
Display.getInstance().onEditingComplete(fPrevTextArea, retVal);
}
});
}
InPlaceEditView.setEditedTextField(textArea);
nextTextArea = null;
} else {
isEditedFieldSwitch = false;
final InPlaceEditView instance = sInstance;
if (instance != null && instance.mEditText != null && instance.mEditText.mTextArea == textArea) {
instance.showTextEditorAgain();
return;
}
if (!isClosing && sInstance != null && sInstance.mEditText != null) {
isClosing = true;
impl.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
instance.endEditing(REASON_UNDEFINED, true);
}
});
}
afterClose = new Runnable() {
@Override
public void run() {
impl.callHideTextEditor();
Display.getInstance().editString(component, textArea.getMaxSize(), inputType, textArea.getText());
}
};
return;
}
} else {
isEditedFieldSwitch = false;
}
mIsEditing = true;
isClosing = false;
afterClose = null;
}
impl.setAsyncEditMode(asyncEdit);
//textArea.setPreferredSize(prefSize);
if (!impl.isAsyncEditMode() && textArea instanceof TextField) {
((TextField) textArea).setEditable(false);
}
final boolean scrollableParent = isScrollableParent(textArea);
// We wrap the text area so that we can safely pass data across to the
// android UI thread.
final TextAreaData textAreaData = new TextAreaData(textArea);
impl.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isEditedFieldSwitch) {
releaseEdit();
if (sInstance == null) {
sInstance = new InPlaceEditView(impl);
impl.relativeLayout.addView(sInstance);
}
// Let's try something new here
// We'll ALWAYS try resize edit mode (since it just works better)
// But we'll detect whether the field is still covered by the keyboard
// and switch to pan mode if necessary.
}
if(scrollableParent || parentForm.isFormBottomPaddingEditingMode()){
setEditMode(true);
}else{
trySetEditMode(true);
}
sInstance.startEditing(impl.getActivity(), textAreaData, initialText, inputType, isEditedFieldSwitch);
}
});
final String[] out = new String[1];
// In order to reuse the code the runs after edit completion, we will wrap it in a runnable
// For sync edit mode, we will just run onComplete.run() at the end of this method. For
// Async mode we add the Runnable to the textarea as a client property, then run it
// when editing eventually completes.
Runnable onComplete = new Runnable() {
public void run() {
if (!impl.isAsyncEditMode() && textArea instanceof TextField) {
((TextField) textArea).setEditable(true);
}
textArea.setPreferredSize(null);
if(sInstance != null && sInstance.mLastEditText != null && sInstance.mLastEditText.mTextArea == textArea){
String retVal = sInstance.mLastEditText.getText().toString();
if (!impl.isAsyncEditMode()) {
sInstance.mLastEditText = null;
impl.getActivity().runOnUiThread(new Runnable() {
public void run() {
releaseEdit();
}
});
}
out[0] = retVal;
}else{
out[0] = initialText;
}
Display.getInstance().onEditingComplete(component, out[0]);
if (impl.isAsyncEditMode()) {
impl.callHideTextEditor();
} else {
// the call to releaseEdit above should remove the native text editor and
// set sInstance to null
// We would like to wait for that to happen before we release our isEditing
// lock.
if (sInstance != null) {
Display.getInstance().invokeAndBlock(new Runnable() {
public void run() {
while (sInstance != null) {
com.codename1.io.Util.sleep(5);
}
}
});
}
}
// Release the editing flag
synchronized (editingLock) {
mIsEditing = false;
}
// If anyone attempted to call edit() while we were still editing,
// the last such attempt will have been added to the afterClose handler
// as a runnable ... this should take priority over the "nextTextArea" setting
if (afterClose != null) {
Display.getInstance().callSerially(afterClose);
} else if (nextTextArea != null) {
final TextArea next = nextTextArea;
nextTextArea = null;
next.requestFocus();
Display.getInstance().callSerially(new Runnable() {
public void run() {
Display.getInstance().editString(next,
next.getMaxSize(),
next.getConstraint(),
next.getText());
}
});
}
}
};
textArea.requestFocus();
textArea.repaint();
if (impl.isAsyncEditMode()) {
component.putClientProperty("android.onAsyncEditingComplete", onComplete);
return;
}
// Make this call synchronous
// We set this flag so that waitForEditCompletion can block on it.
// The flag will be released inside the endEditing method which will
// allow the method to proceed.
waitingForSynchronousEditingCompletion = true;
waitForEditCompletion();
onComplete.run();
}
public static void setEditedTextField(final TextArea textarea) {
Display display = Display.getInstance();
Runnable task = new Runnable(){
public void run() {
AndroidImplementation.getInstance().setFocusedEditingText(textarea);
}
};
if (display.isEdt()) {
task.run();
} else {
display.callSeriallyAndWait(task);
}
}
private static boolean isScrollableParent(Component c){
Container p = c.getParent();
Font f = c.getStyle().getFont();
float pixelSize = f == null ? Display.getInstance().convertToPixels(4) : f.getPixelSize();
while( p != null){
if(p.isScrollableY() && p.getAbsoluteY() + p.getScrollY() < Display.getInstance().getDisplayHeight() / 2 - pixelSize * 2){
return true;
}
p = p.getParent();
}
return false;
}
private class DebugResultReceiver extends ResultReceiver {
private static final String TAG = "InPlaceEditView.ResultReceiver";
private SparseArray<String> mResultToStringMap = new SparseArray<String>();
public DebugResultReceiver(Handler handler) {
super(handler);
mResultToStringMap.append(InputMethodManager.RESULT_HIDDEN, "RESULT_HIDDEN");
mResultToStringMap.append(InputMethodManager.RESULT_SHOWN, "RESULT_SHOWN");
mResultToStringMap.append(InputMethodManager.RESULT_UNCHANGED_HIDDEN, "RESULT_UNCHANGED_HIDDEN");
mResultToStringMap.append(InputMethodManager.RESULT_UNCHANGED_SHOWN, "RESULT_UNCHANGED_SHOWN");
}
protected void onReceiveResult(int resultCode, Bundle resultData) {
String resultStr = mResultToStringMap.get(resultCode, "Unknown");
Log.i(TAG, "resultCode = " + resultStr);
}
}
private class TextChange {
String textToAppend;
int atPos;
int deleteLength;
}
class EditView extends AutoCompleteTextView {
private InPlaceEditView mInPlaceEditView;
private TextArea mTextArea = null;
TextArea getTextArea() {
return mTextArea;
}
private ResetableTextWatcher mTextWatcher = new ResetableTextWatcher() {
private boolean started = false;
private TextChange currChange;
private int lastInsertStartPos;
private int lastInsertBeforeCount;
private int lastInsertAfterCount;
/**
* Reset status after connected textarea change.
*/
@Override
public void reset() {
started = false;
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// We use this hook to catch keyboard strokes in async edit mode while the
// edit text field is hidden.
currChange = new TextChange();
currChange.atPos = start;
lastInsertAfterCount = after;
lastInsertBeforeCount = count;
lastInsertStartPos = start;
if (mIsEditing && impl.isAsyncEditMode() && isTextEditorHidden()) {
// If the text editor is hidden, and the user starts typing in the
// keyboard (because we're in async edit mode), then we need to
// trigger the native editor to display again.
showTextEditorAgain();
}
}
@Override
public void afterTextChanged(final Editable s) {
if (isEditing() && mTextArea != null) {
try {
final String actualString = s.toString();
//make sure to start send events to the cn1 textfield only
//when the first string equals to the initial text
if (!started) {
if (mTextArea.getText().equals(actualString)) {
started = true;
}
return;
}
synchronized (InPlaceEditView.this) {
// In Async Edit mode, we may have just triggered a "showTextEditorAgain" in the
// beforeTextChanged event. However this will trigger some async stuff on the
// EDT and the UI thread to initialize the native editor with the contents
// of the CN1 TextArea. That created an inputBuffer to catch key strokes in
// the mean time so that they will be added correctly to the
// native editor when it is ready.
if (inputBuffer != null) {
if (lastInsertBeforeCount > lastInsertAfterCount) {
currChange.deleteLength = lastInsertBeforeCount - lastInsertAfterCount;
inputBuffer.add(currChange);
currChange = null;
} else if (lastInsertBeforeCount < lastInsertAfterCount) {
currChange.textToAppend = actualString.substring(lastInsertStartPos, actualString.length() - lastInsertAfterCount + 1);
inputBuffer.add(currChange);
currChange = null;
}
}
}
Display.getInstance().callSerially(new Runnable() {
@Override
public void run() {
if (!actualString.equals(mTextArea.getText())) {
mTextArea.setText(actualString);
}
}
});
} catch (Exception e) {
Log.e(TAG, e.toString() + " " + Log.getStackTraceString(e));
}
}
}
};
/**
* Constructor
* @param context
* @param inPlaceEditView
*/
public EditView(Context context, TextArea textArea, InPlaceEditView inPlaceEditView, int style) {
super(context, null, style);
mInPlaceEditView = inPlaceEditView;
mTextArea = textArea;
setBackgroundColor(Color.TRANSPARENT);
}
/**
* Connects to other textArea.
*/
public void switchToTextArea(TextArea other) {
if (this.mTextArea != null && this.mTextArea != other) {
Display.getInstance().onEditingComplete(this.mTextArea, this.mTextArea.getText());
}
this.mTextArea = other;
mTextWatcher.reset();
}
@Override
public void onEditorAction(int actionCode) {
Runnable task = mInPlaceEditView.onEditorAction(actionCode);
if (task != null) {
super.onEditorAction(actionCode);
task.run();
}
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
endEditing(InPlaceEditView.REASON_SYSTEM_KEY, false, true);
return true;
}
return super.onKeyPreIme(keyCode, event);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// If the user presses the back button, or the menu button
// we must terminate editing, to allow EDT to handle events
// again
if (keyCode == KeyEvent.KEYCODE_BACK
|| keyCode == KeyEvent.KEYCODE_MENU) {
endEditing(InPlaceEditView.REASON_SYSTEM_KEY, false, true);
}
return super.onKeyDown(keyCode, event);
}
}
public class MyPasswordTransformationMethod extends PasswordTransformationMethod {
@Override
public CharSequence getTransformation(CharSequence source, View view) {
return new PasswordCharSequence(source);
}
private class PasswordCharSequence implements CharSequence {
private CharSequence mSource;
public PasswordCharSequence(CharSequence source) {
mSource = source; // Store char sequence
}
public char charAt(int index) {
return '\u25CF'; // This is the important part
}
public int length() {
return mSource.length(); // Return default
}
public CharSequence subSequence(int start, int end) {
return mSource.subSequence(start, end); // Return default
}
}
};
}
interface ResetableTextWatcher extends TextWatcher {
public void reset();
}