mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
1123 lines
39 KiB
Java
1123 lines
39 KiB
Java
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
|
* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Mozilla Android code.
|
|
*
|
|
* The Initial Developer of the Original Code is Mozilla Foundation.
|
|
* Portions created by the Initial Developer are Copyright (C) 2010
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Michael Wu <mwu@mozilla.com>
|
|
* Alex Pakhotin <alexp@mozilla.com>
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
package org.mozilla.gecko;
|
|
|
|
import java.io.*;
|
|
import java.util.*;
|
|
import java.util.concurrent.*;
|
|
import java.util.concurrent.atomic.*;
|
|
|
|
import org.mozilla.gecko.gfx.InputConnectionHandler;
|
|
|
|
import android.os.*;
|
|
import android.app.*;
|
|
import android.text.*;
|
|
import android.text.style.*;
|
|
import android.view.*;
|
|
import android.view.inputmethod.*;
|
|
import android.content.*;
|
|
import android.R;
|
|
import android.text.method.TextKeyListener;
|
|
import android.text.method.KeyListener;
|
|
import android.util.*;
|
|
|
|
public class GeckoInputConnection
|
|
extends BaseInputConnection
|
|
implements TextWatcher, InputConnectionHandler
|
|
{
|
|
private static final boolean DEBUG = false;
|
|
protected static final String LOGTAG = "GeckoInputConnection";
|
|
|
|
public static GeckoInputConnection create(View targetView) {
|
|
if (DEBUG)
|
|
return new DebugGeckoInputConnection(targetView);
|
|
else
|
|
return new GeckoInputConnection(targetView);
|
|
}
|
|
|
|
protected GeckoInputConnection(View targetView) {
|
|
super(targetView, true);
|
|
mQueryResult = new SynchronousQueue<String>();
|
|
|
|
mEditableFactory = Editable.Factory.getInstance();
|
|
initEditable("");
|
|
mIMEState = IME_STATE_DISABLED;
|
|
mIMETypeHint = "";
|
|
mIMEActionHint = "";
|
|
}
|
|
|
|
@Override
|
|
public boolean beginBatchEdit() {
|
|
mBatchMode = true;
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean endBatchEdit() {
|
|
mBatchMode = false;
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean commitCompletion(CompletionInfo text) {
|
|
return commitText(text.getText(), 1);
|
|
}
|
|
|
|
@Override
|
|
public boolean commitText(CharSequence text, int newCursorPosition) {
|
|
replaceText(text, newCursorPosition, false);
|
|
|
|
if (mComposing) {
|
|
if (DEBUG) Log.d(LOGTAG, ". . . commitText: endComposition");
|
|
endComposition();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean finishComposingText() {
|
|
if (mComposing) {
|
|
if (DEBUG) Log.d(LOGTAG, ". . . finishComposingText: endComposition");
|
|
endComposition();
|
|
}
|
|
|
|
final Editable content = getEditable();
|
|
if (content != null) {
|
|
beginBatchEdit();
|
|
removeComposingSpans(content);
|
|
endBatchEdit();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public Editable getEditable() {
|
|
return mEditable;
|
|
}
|
|
|
|
@Override
|
|
public boolean performContextMenuAction(int id) {
|
|
final Editable content = getEditable();
|
|
if (content == null)
|
|
return false;
|
|
|
|
String text = content.toString();
|
|
|
|
int a = Selection.getSelectionStart(content);
|
|
int b = Selection.getSelectionEnd(content);
|
|
if (a < 0) a = 0;
|
|
if (b < 0) b = 0;
|
|
if (a > b) {
|
|
int tmp = a;
|
|
a = b;
|
|
b = tmp;
|
|
}
|
|
|
|
switch (id) {
|
|
case R.id.selectAll:
|
|
setSelection(0, text.length());
|
|
break;
|
|
case R.id.cut:
|
|
// Fill the clipboard
|
|
GeckoAppShell.setClipboardText(text);
|
|
// If selection is empty, we'll select everything
|
|
if (a >= b)
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_SET_SELECTION, 0, text.length()));
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_DELETE_TEXT, 0, 0));
|
|
break;
|
|
case R.id.paste:
|
|
commitText(GeckoAppShell.getClipboardText(), 1);
|
|
break;
|
|
case R.id.copy:
|
|
// If there is no selection set, we must be doing "Copy All",
|
|
// otherwise get the selection
|
|
if (a < b)
|
|
text = text.substring(a, b);
|
|
GeckoAppShell.setClipboardText(text.substring(a, b));
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public ExtractedText getExtractedText(ExtractedTextRequest req, int flags) {
|
|
if (req == null)
|
|
return null;
|
|
|
|
final Editable content = getEditable();
|
|
if (content == null)
|
|
return null;
|
|
|
|
if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0)
|
|
mUpdateRequest = req;
|
|
|
|
ExtractedText extract = new ExtractedText();
|
|
extract.flags = 0;
|
|
extract.partialStartOffset = -1;
|
|
extract.partialEndOffset = -1;
|
|
|
|
int a = Selection.getSelectionStart(content);
|
|
int b = Selection.getSelectionEnd(content);
|
|
if (a > b) {
|
|
int tmp = a;
|
|
a = b;
|
|
b = tmp;
|
|
}
|
|
|
|
extract.selectionStart = a;
|
|
extract.selectionEnd = b;
|
|
|
|
extract.startOffset = 0;
|
|
extract.text = content.toString();
|
|
|
|
return extract;
|
|
}
|
|
|
|
@Override
|
|
public boolean setSelection(int start, int end) {
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_SET_SELECTION, start, end - start));
|
|
|
|
return super.setSelection(start, end);
|
|
}
|
|
|
|
@Override
|
|
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
|
replaceText(text, newCursorPosition, true);
|
|
return true;
|
|
}
|
|
|
|
private void replaceText(CharSequence text, int newCursorPosition, boolean composing) {
|
|
if (DEBUG) Log.d(LOGTAG, String.format("IME: replaceText(\"%s\", %d, %s)", text, newCursorPosition, composing?"true":"false"));
|
|
|
|
if (text == null)
|
|
text = "";
|
|
|
|
final Editable content = getEditable();
|
|
if (content == null) {
|
|
return;
|
|
}
|
|
|
|
beginBatchEdit();
|
|
|
|
// delete composing text set previously.
|
|
int a = getComposingSpanStart(content);
|
|
int b = getComposingSpanEnd(content);
|
|
|
|
if (DEBUG) Log.d(LOGTAG, "Composing span: " + a + " to " + b);
|
|
|
|
if (b < a) {
|
|
int tmp = a;
|
|
a = b;
|
|
b = tmp;
|
|
}
|
|
|
|
if (a != -1 && b != -1) {
|
|
removeComposingSpans(content);
|
|
} else {
|
|
a = Selection.getSelectionStart(content);
|
|
b = Selection.getSelectionEnd(content);
|
|
if (a < 0) a = 0;
|
|
if (b < 0) b = 0;
|
|
if (b < a) {
|
|
int tmp = a;
|
|
a = b;
|
|
b = tmp;
|
|
}
|
|
}
|
|
|
|
if (composing) {
|
|
Spannable sp = null;
|
|
if (!(text instanceof Spannable)) {
|
|
sp = new SpannableStringBuilder(text);
|
|
text = sp;
|
|
sp.setSpan(COMPOSING_SPAN, 0, sp.length(),
|
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
|
|
} else {
|
|
sp = (Spannable)text;
|
|
}
|
|
setComposingSpans(sp);
|
|
}
|
|
|
|
if (DEBUG) Log.d(LOGTAG, "Replacing from " + a + " to " + b + " with \""
|
|
+ text + "\", composing=" + composing
|
|
+ ", type=" + text.getClass().getCanonicalName());
|
|
|
|
if (DEBUG) {
|
|
LogPrinter lp = new LogPrinter(Log.VERBOSE, LOGTAG);
|
|
lp.println("Current text:");
|
|
TextUtils.dumpSpans(content, lp, " ");
|
|
lp.println("Composing text:");
|
|
TextUtils.dumpSpans(text, lp, " ");
|
|
}
|
|
|
|
// Position the cursor appropriately, so that after replacing the
|
|
// desired range of text it will be located in the correct spot.
|
|
// This allows us to deal with filters performing edits on the text
|
|
// we are providing here.
|
|
if (newCursorPosition > 0) {
|
|
newCursorPosition += b - 1;
|
|
} else {
|
|
newCursorPosition += a;
|
|
}
|
|
if (newCursorPosition < 0) newCursorPosition = 0;
|
|
if (newCursorPosition > content.length())
|
|
newCursorPosition = content.length();
|
|
Selection.setSelection(content, newCursorPosition);
|
|
|
|
content.replace(a, b, text);
|
|
|
|
if (DEBUG) {
|
|
LogPrinter lp = new LogPrinter(Log.VERBOSE, LOGTAG);
|
|
lp.println("Final text:");
|
|
TextUtils.dumpSpans(content, lp, " ");
|
|
}
|
|
|
|
endBatchEdit();
|
|
}
|
|
|
|
@Override
|
|
public boolean setComposingRegion(int start, int end) {
|
|
if (mComposing) {
|
|
if (DEBUG) Log.d(LOGTAG, ". . . setComposingRegion: endComposition");
|
|
endComposition();
|
|
}
|
|
|
|
return super.setComposingRegion(start, end);
|
|
}
|
|
|
|
public String getComposingText() {
|
|
final Editable content = getEditable();
|
|
if (content == null) {
|
|
return null;
|
|
}
|
|
int a = getComposingSpanStart(content);
|
|
int b = getComposingSpanEnd(content);
|
|
|
|
if (a < 0 || b < 0)
|
|
return null;
|
|
|
|
if (b < a) {
|
|
int tmp = a;
|
|
a = b;
|
|
b = tmp;
|
|
}
|
|
|
|
return TextUtils.substring(content, a, b);
|
|
}
|
|
|
|
public boolean onKeyDel() {
|
|
// Some IMEs don't update us on deletions
|
|
// In that case we are not updated when a composition
|
|
// is destroyed, and Bad Things happen
|
|
|
|
if (!mComposing)
|
|
return false;
|
|
|
|
String text = getComposingText();
|
|
|
|
if (text != null && text.length() > 1) {
|
|
text = text.substring(0, text.length() - 1);
|
|
replaceText(text, 1, false);
|
|
return false;
|
|
}
|
|
|
|
commitText(null, 1);
|
|
return true;
|
|
}
|
|
|
|
public void notifyTextChange(InputMethodManager imm, String text,
|
|
int start, int oldEnd, int newEnd) {
|
|
if (!mBatchMode) {
|
|
if (!text.contentEquals(mEditable)) {
|
|
if (DEBUG) Log.d(LOGTAG, String.format(". . . notifyTextChange: current mEditable=\"%s\"",
|
|
mEditable.toString()));
|
|
setEditable(text);
|
|
}
|
|
}
|
|
|
|
if (mUpdateRequest == null)
|
|
return;
|
|
|
|
View v = GeckoApp.mAppContext.getLayerController().getView();
|
|
|
|
if (imm == null) {
|
|
imm = (InputMethodManager)v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
if (imm == null)
|
|
return;
|
|
}
|
|
|
|
mUpdateExtract.flags = 0;
|
|
|
|
// We update from (0, oldEnd) to (0, newEnd) because some Android IMEs
|
|
// assume that updates start at zero, according to jchen.
|
|
mUpdateExtract.partialStartOffset = 0;
|
|
mUpdateExtract.partialEndOffset = oldEnd;
|
|
|
|
// Faster to not query for selection
|
|
mUpdateExtract.selectionStart = newEnd;
|
|
mUpdateExtract.selectionEnd = newEnd;
|
|
|
|
mUpdateExtract.text = text.substring(0, newEnd);
|
|
mUpdateExtract.startOffset = 0;
|
|
|
|
imm.updateExtractedText(v, mUpdateRequest.token, mUpdateExtract);
|
|
}
|
|
|
|
public void notifySelectionChange(InputMethodManager imm,
|
|
int start, int end) {
|
|
if (!mBatchMode) {
|
|
final Editable content = getEditable();
|
|
int a = Selection.getSelectionStart(content);
|
|
int b = Selection.getSelectionEnd(content);
|
|
if (start != a || end != b) {
|
|
if (DEBUG) Log.d(LOGTAG, String.format(". . . notifySelectionChange: current editable selection: [%d, %d]", a, b));
|
|
super.setSelection(start, end);
|
|
|
|
// Check if the selection is inside composing span
|
|
int ca = getComposingSpanStart(content);
|
|
int cb = getComposingSpanEnd(content);
|
|
if (cb < ca) {
|
|
int tmp = ca;
|
|
ca = cb;
|
|
cb = tmp;
|
|
}
|
|
if (start < ca || start > cb || end < ca || end > cb) {
|
|
if (DEBUG) Log.d(LOGTAG, ". . . notifySelectionChange: removeComposingSpans");
|
|
removeComposingSpans(content);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (imm != null && imm.isFullscreenMode()) {
|
|
View v = GeckoApp.mAppContext.getLayerController().getView();
|
|
imm.updateSelection(v, start, end, -1, -1);
|
|
}
|
|
}
|
|
|
|
public void reset() {
|
|
mComposing = false;
|
|
mBatchMode = false;
|
|
mUpdateRequest = null;
|
|
}
|
|
|
|
// TextWatcher
|
|
public void onTextChanged(CharSequence s, int start, int before, int count)
|
|
{
|
|
if (!mComposing) {
|
|
if (DEBUG) Log.d(LOGTAG, ". . . onTextChanged: IME_COMPOSITION_BEGIN");
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_COMPOSITION_BEGIN, 0, 0));
|
|
mComposing = true;
|
|
|
|
if (DEBUG) Log.d(LOGTAG, ". . . onTextChanged: IME_SET_SELECTION, start=" + start + ", len=" + before);
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_SET_SELECTION, start, before));
|
|
}
|
|
|
|
if (count == 0) {
|
|
if (DEBUG) Log.d(LOGTAG, ". . . onTextChanged: IME_DELETE_TEXT");
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_DELETE_TEXT, 0, 0));
|
|
} else {
|
|
sendTextToGecko(s.subSequence(start, start + count), start + count);
|
|
}
|
|
|
|
if (DEBUG) Log.d(LOGTAG, ". . . onTextChanged: IME_SET_SELECTION, start=" + (start + count) + ", 0");
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_SET_SELECTION, start + count, 0));
|
|
|
|
// Block this thread until all pending events are processed
|
|
GeckoAppShell.geckoEventSync();
|
|
}
|
|
|
|
private void endComposition() {
|
|
if (DEBUG) Log.d(LOGTAG, "IME: endComposition: IME_COMPOSITION_END");
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_COMPOSITION_END, 0, 0));
|
|
mComposing = false;
|
|
}
|
|
|
|
private void sendTextToGecko(CharSequence text, int caretPos) {
|
|
if (DEBUG) Log.d(LOGTAG, "IME: sendTextToGecko(\"" + text + "\")");
|
|
|
|
// Handle composition text styles
|
|
if (text != null && text instanceof Spanned) {
|
|
Spanned span = (Spanned) text;
|
|
int spanStart = 0, spanEnd = 0;
|
|
boolean pastSelStart = false, pastSelEnd = false;
|
|
|
|
do {
|
|
int rangeType = GeckoEvent.IME_RANGE_CONVERTEDTEXT;
|
|
int rangeStyles = 0, rangeForeColor = 0, rangeBackColor = 0;
|
|
|
|
// Find next offset where there is a style transition
|
|
spanEnd = span.nextSpanTransition(spanStart + 1, text.length(),
|
|
CharacterStyle.class);
|
|
|
|
// Empty range, continue
|
|
if (spanEnd <= spanStart)
|
|
continue;
|
|
|
|
// Get and iterate through list of span objects within range
|
|
CharacterStyle styles[] = span.getSpans(
|
|
spanStart, spanEnd, CharacterStyle.class);
|
|
|
|
for (CharacterStyle style : styles) {
|
|
if (style instanceof UnderlineSpan) {
|
|
// Text should be underlined
|
|
rangeStyles |= GeckoEvent.IME_RANGE_UNDERLINE;
|
|
|
|
} else if (style instanceof ForegroundColorSpan) {
|
|
// Text should be of a different foreground color
|
|
rangeStyles |= GeckoEvent.IME_RANGE_FORECOLOR;
|
|
rangeForeColor =
|
|
((ForegroundColorSpan)style).getForegroundColor();
|
|
|
|
} else if (style instanceof BackgroundColorSpan) {
|
|
// Text should be of a different background color
|
|
rangeStyles |= GeckoEvent.IME_RANGE_BACKCOLOR;
|
|
rangeBackColor =
|
|
((BackgroundColorSpan)style).getBackgroundColor();
|
|
}
|
|
}
|
|
|
|
// Add range to array, the actual styles are
|
|
// applied when IME_SET_TEXT is sent
|
|
if (DEBUG) Log.d(LOGTAG, String.format(". . . sendTextToGecko: IME_ADD_RANGE, %d, %d, %d, %d, %d, %d",
|
|
spanStart, spanEnd - spanStart, rangeType, rangeStyles, rangeForeColor, rangeBackColor));
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(spanStart, spanEnd - spanStart,
|
|
rangeType, rangeStyles,
|
|
rangeForeColor, rangeBackColor));
|
|
|
|
spanStart = spanEnd;
|
|
} while (spanStart < text.length());
|
|
} else {
|
|
if (DEBUG) Log.d(LOGTAG, ". . . sendTextToGecko: IME_ADD_RANGE, 0, " + text.length() +
|
|
", IME_RANGE_RAWINPUT, IME_RANGE_UNDERLINE)");
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(0, text == null ? 0 : text.length(),
|
|
GeckoEvent.IME_RANGE_RAWINPUT,
|
|
GeckoEvent.IME_RANGE_UNDERLINE, 0, 0));
|
|
}
|
|
|
|
// Change composition (treating selection end as where the caret is)
|
|
if (DEBUG) Log.d(LOGTAG, ". . . sendTextToGecko: IME_SET_TEXT, IME_RANGE_CARETPOSITION, \"" + text + "\")");
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(caretPos, 0,
|
|
GeckoEvent.IME_RANGE_CARETPOSITION, 0, 0, 0,
|
|
text.toString()));
|
|
}
|
|
|
|
public void afterTextChanged(Editable s)
|
|
{
|
|
}
|
|
|
|
public void beforeTextChanged(CharSequence s, int start, int count, int after)
|
|
{
|
|
}
|
|
|
|
public InputConnection onCreateInputConnection(EditorInfo outAttrs)
|
|
{
|
|
outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
|
|
outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE;
|
|
outAttrs.actionLabel = null;
|
|
mKeyListener = TextKeyListener.getInstance();
|
|
|
|
if (mIMEState == IME_STATE_PASSWORD)
|
|
outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
|
|
else if (mIMETypeHint.equalsIgnoreCase("url"))
|
|
outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_URI;
|
|
else if (mIMETypeHint.equalsIgnoreCase("email"))
|
|
outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
|
|
else if (mIMETypeHint.equalsIgnoreCase("search"))
|
|
outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH;
|
|
else if (mIMETypeHint.equalsIgnoreCase("tel"))
|
|
outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
|
|
else if (mIMETypeHint.equalsIgnoreCase("number") ||
|
|
mIMETypeHint.equalsIgnoreCase("range"))
|
|
outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;
|
|
else if (mIMETypeHint.equalsIgnoreCase("datetime") ||
|
|
mIMETypeHint.equalsIgnoreCase("datetime-local"))
|
|
outAttrs.inputType = InputType.TYPE_CLASS_DATETIME |
|
|
InputType.TYPE_DATETIME_VARIATION_NORMAL;
|
|
else if (mIMETypeHint.equalsIgnoreCase("date"))
|
|
outAttrs.inputType = InputType.TYPE_CLASS_DATETIME |
|
|
InputType.TYPE_DATETIME_VARIATION_DATE;
|
|
else if (mIMETypeHint.equalsIgnoreCase("time"))
|
|
outAttrs.inputType = InputType.TYPE_CLASS_DATETIME |
|
|
InputType.TYPE_DATETIME_VARIATION_TIME;
|
|
|
|
if (mIMEActionHint.equalsIgnoreCase("go"))
|
|
outAttrs.imeOptions = EditorInfo.IME_ACTION_GO;
|
|
else if (mIMEActionHint.equalsIgnoreCase("done"))
|
|
outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
|
|
else if (mIMEActionHint.equalsIgnoreCase("next"))
|
|
outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT;
|
|
else if (mIMEActionHint.equalsIgnoreCase("search"))
|
|
outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH;
|
|
else if (mIMEActionHint.equalsIgnoreCase("send"))
|
|
outAttrs.imeOptions = EditorInfo.IME_ACTION_SEND;
|
|
else if (mIMEActionHint != null && mIMEActionHint.length() != 0)
|
|
outAttrs.actionLabel = mIMEActionHint;
|
|
|
|
if (mIMELandscapeFS == false)
|
|
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI;
|
|
|
|
reset();
|
|
return this;
|
|
}
|
|
|
|
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
|
switch (event.getAction()) {
|
|
case KeyEvent.ACTION_DOWN:
|
|
return processKeyDown(keyCode, event, true);
|
|
case KeyEvent.ACTION_UP:
|
|
return processKeyUp(keyCode, event, true);
|
|
case KeyEvent.ACTION_MULTIPLE:
|
|
return onKeyMultiple(keyCode, event.getRepeatCount(), event);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
return processKeyDown(keyCode, event, false);
|
|
}
|
|
|
|
private boolean processKeyDown(int keyCode, KeyEvent event, boolean isPreIme) {
|
|
if (DEBUG) Log.d(LOGTAG, "IME: processKeyDown(keyCode=" + keyCode + ", event=" + event + ", " + isPreIme + ")");
|
|
|
|
switch (keyCode) {
|
|
case KeyEvent.KEYCODE_MENU:
|
|
case KeyEvent.KEYCODE_BACK:
|
|
case KeyEvent.KEYCODE_VOLUME_UP:
|
|
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
|
case KeyEvent.KEYCODE_SEARCH:
|
|
return false;
|
|
case KeyEvent.KEYCODE_DEL:
|
|
// See comments in GeckoInputConnection.onKeyDel
|
|
if (onKeyDel()) {
|
|
return true;
|
|
}
|
|
break;
|
|
case KeyEvent.KEYCODE_ENTER:
|
|
if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0 &&
|
|
mIMEActionHint.equalsIgnoreCase("next"))
|
|
event = new KeyEvent(event.getAction(), KeyEvent.KEYCODE_TAB);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (isPreIme && mIMEState != IME_STATE_DISABLED &&
|
|
(event.getMetaState() & KeyEvent.META_ALT_ON) != 0)
|
|
// Let active IME process pre-IME key events
|
|
return false;
|
|
|
|
View v = GeckoApp.mAppContext.getLayerController().getView();
|
|
|
|
// KeyListener returns true if it handled the event for us.
|
|
if (mIMEState == IME_STATE_DISABLED ||
|
|
keyCode == KeyEvent.KEYCODE_ENTER ||
|
|
keyCode == KeyEvent.KEYCODE_DEL ||
|
|
keyCode == KeyEvent.KEYCODE_TAB ||
|
|
(event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0 ||
|
|
!mKeyListener.onKeyDown(v, mEditable, keyCode, event)) {
|
|
// Make sure selection in Gecko is up-to-date
|
|
final Editable content = getEditable();
|
|
int a = Selection.getSelectionStart(content);
|
|
int b = Selection.getSelectionEnd(content);
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_SET_SELECTION, a, b - a));
|
|
|
|
GeckoAppShell.sendEventToGecko(new GeckoEvent(event));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
|
return processKeyUp(keyCode, event, false);
|
|
}
|
|
|
|
private boolean processKeyUp(int keyCode, KeyEvent event, boolean isPreIme) {
|
|
if (DEBUG) Log.d(LOGTAG, "IME: processKeyUp(keyCode=" + keyCode + ", event=" + event + ", " + isPreIme + ")");
|
|
|
|
switch (keyCode) {
|
|
case KeyEvent.KEYCODE_BACK:
|
|
case KeyEvent.KEYCODE_SEARCH:
|
|
case KeyEvent.KEYCODE_MENU:
|
|
return false;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (isPreIme && mIMEState != IME_STATE_DISABLED &&
|
|
(event.getMetaState() & KeyEvent.META_ALT_ON) != 0)
|
|
// Let active IME process pre-IME key events
|
|
return false;
|
|
|
|
View v = GeckoApp.mAppContext.getLayerController().getView();
|
|
|
|
if (mIMEState == IME_STATE_DISABLED ||
|
|
keyCode == KeyEvent.KEYCODE_ENTER ||
|
|
keyCode == KeyEvent.KEYCODE_DEL ||
|
|
(event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0 ||
|
|
!mKeyListener.onKeyUp(v, mEditable, keyCode, event))
|
|
GeckoAppShell.sendEventToGecko(new GeckoEvent(event));
|
|
return true;
|
|
}
|
|
|
|
public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
|
|
GeckoAppShell.sendEventToGecko(new GeckoEvent(event));
|
|
return true;
|
|
}
|
|
|
|
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
|
|
View v = GeckoApp.mAppContext.getLayerController().getView();
|
|
switch (keyCode) {
|
|
case KeyEvent.KEYCODE_MENU:
|
|
InputMethodManager imm = (InputMethodManager)
|
|
v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
imm.toggleSoftInputFromWindow(v.getWindowToken(),
|
|
imm.SHOW_FORCED, 0);
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public boolean isIMEEnabled() {
|
|
// make sure this picks up PASSWORD and PLUGIN states as well
|
|
return mIMEState != IME_STATE_DISABLED;
|
|
}
|
|
|
|
public void notifyIME(int type, int state) {
|
|
View v = GeckoApp.mAppContext.getLayerController().getView();
|
|
|
|
if (v == null)
|
|
return;
|
|
|
|
switch (type) {
|
|
case NOTIFY_IME_RESETINPUTSTATE:
|
|
if (DEBUG) Log.d(LOGTAG, ". . . notifyIME: reset");
|
|
|
|
// Composition event is already fired from widget.
|
|
// So reset IME flags.
|
|
reset();
|
|
|
|
// Don't use IMEStateUpdater for reset.
|
|
// Because IME may not work showSoftInput()
|
|
// after calling restartInput() immediately.
|
|
// So we have to call showSoftInput() delay.
|
|
InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
if (imm == null) {
|
|
// no way to reset IME status directly
|
|
IMEStateUpdater.resetIME();
|
|
} else {
|
|
imm.restartInput(v);
|
|
}
|
|
|
|
// keep current enabled state
|
|
IMEStateUpdater.enableIME();
|
|
break;
|
|
|
|
case NOTIFY_IME_CANCELCOMPOSITION:
|
|
if (DEBUG) Log.d(LOGTAG, ". . . notifyIME: cancel");
|
|
IMEStateUpdater.resetIME();
|
|
break;
|
|
|
|
case NOTIFY_IME_FOCUSCHANGE:
|
|
if (DEBUG) Log.d(LOGTAG, ". . . notifyIME: focus");
|
|
IMEStateUpdater.resetIME();
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void notifyIMEEnabled(int state, String typeHint,
|
|
String actionHint, boolean landscapeFS)
|
|
{
|
|
View v = GeckoApp.mAppContext.getLayerController().getView();
|
|
|
|
if (v == null)
|
|
return;
|
|
|
|
/* When IME is 'disabled', IME processing is disabled.
|
|
In addition, the IME UI is hidden */
|
|
mIMEState = state;
|
|
mIMETypeHint = typeHint;
|
|
mIMEActionHint = actionHint;
|
|
mIMELandscapeFS = landscapeFS;
|
|
IMEStateUpdater.enableIME();
|
|
}
|
|
|
|
|
|
public void notifyIMEChange(String text, int start, int end, int newEnd) {
|
|
View v = GeckoApp.mAppContext.getLayerController().getView();
|
|
|
|
if (v == null)
|
|
return;
|
|
|
|
InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
if (imm == null)
|
|
return;
|
|
|
|
if (newEnd < 0)
|
|
notifySelectionChange(imm, start, end);
|
|
else
|
|
notifyTextChange(imm, text, start, end, newEnd);
|
|
}
|
|
|
|
public void returnIMEQueryResult(String result, int selectionStart, int selectionLength) {
|
|
mSelectionStart = selectionStart;
|
|
mSelectionLength = selectionLength;
|
|
try {
|
|
mQueryResult.put(result);
|
|
} catch (InterruptedException e) {}
|
|
}
|
|
|
|
static private final Timer mIMETimer = new Timer("GeckoInputConnection Timer");
|
|
|
|
static private final int NOTIFY_IME_RESETINPUTSTATE = 0;
|
|
static private final int NOTIFY_IME_SETOPENSTATE = 1;
|
|
static private final int NOTIFY_IME_CANCELCOMPOSITION = 2;
|
|
static private final int NOTIFY_IME_FOCUSCHANGE = 3;
|
|
|
|
|
|
/* Delay updating IME states (see bug 573800) */
|
|
private static final class IMEStateUpdater extends TimerTask
|
|
{
|
|
static private IMEStateUpdater instance;
|
|
private boolean mEnable, mReset;
|
|
|
|
static private IMEStateUpdater getInstance() {
|
|
if (instance == null) {
|
|
instance = new IMEStateUpdater();
|
|
mIMETimer.schedule(instance, 200);
|
|
}
|
|
return instance;
|
|
}
|
|
|
|
static public synchronized void enableIME() {
|
|
getInstance().mEnable = true;
|
|
}
|
|
|
|
static public synchronized void resetIME() {
|
|
getInstance().mReset = true;
|
|
}
|
|
|
|
public void run() {
|
|
if (DEBUG) Log.d(LOGTAG, "IME: run()");
|
|
synchronized(IMEStateUpdater.class) {
|
|
instance = null;
|
|
}
|
|
|
|
View v = GeckoApp.mAppContext.getLayerController().getView();
|
|
if (DEBUG) Log.d(LOGTAG, "IME: v="+v);
|
|
|
|
InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
if (imm == null)
|
|
return;
|
|
|
|
if (mReset)
|
|
imm.restartInput(v);
|
|
|
|
if (!mEnable)
|
|
return;
|
|
|
|
if (mIMEState != IME_STATE_DISABLED &&
|
|
mIMEState != IME_STATE_PLUGIN)
|
|
imm.showSoftInput(v, 0);
|
|
else
|
|
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
|
|
}
|
|
}
|
|
|
|
public void setEditable(String contents)
|
|
{
|
|
mEditable.removeSpan(this);
|
|
mEditable.replace(0, mEditable.length(), contents);
|
|
mEditable.setSpan(this, 0, contents.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
|
|
Selection.setSelection(mEditable, contents.length());
|
|
}
|
|
|
|
public void initEditable(String contents)
|
|
{
|
|
mEditable = mEditableFactory.newEditable(contents);
|
|
mEditable.setSpan(this, 0, contents.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
|
|
Selection.setSelection(mEditable, contents.length());
|
|
}
|
|
|
|
// Is a composition active?
|
|
boolean mComposing;
|
|
|
|
// IME stuff
|
|
public static final int IME_STATE_DISABLED = 0;
|
|
public static final int IME_STATE_ENABLED = 1;
|
|
public static final int IME_STATE_PASSWORD = 2;
|
|
public static final int IME_STATE_PLUGIN = 3;
|
|
|
|
final CharacterStyle COMPOSING_SPAN = new UnderlineSpan();
|
|
|
|
KeyListener mKeyListener;
|
|
Editable mEditable;
|
|
Editable.Factory mEditableFactory;
|
|
static int mIMEState;
|
|
static String mIMETypeHint;
|
|
static String mIMEActionHint;
|
|
static boolean mIMELandscapeFS;
|
|
|
|
private boolean mBatchMode;
|
|
|
|
ExtractedTextRequest mUpdateRequest;
|
|
final ExtractedText mUpdateExtract = new ExtractedText();
|
|
|
|
int mSelectionStart, mSelectionLength;
|
|
SynchronousQueue<String> mQueryResult;
|
|
}
|
|
|
|
class DebugGeckoInputConnection
|
|
extends GeckoInputConnection
|
|
{
|
|
public DebugGeckoInputConnection(View targetView) {
|
|
super(targetView);
|
|
}
|
|
|
|
@Override
|
|
public boolean beginBatchEdit() {
|
|
Log.d(LOGTAG, "IME: beginBatchEdit");
|
|
return super.beginBatchEdit();
|
|
}
|
|
|
|
@Override
|
|
public boolean endBatchEdit() {
|
|
Log.d(LOGTAG, "IME: endBatchEdit");
|
|
return super.endBatchEdit();
|
|
}
|
|
|
|
@Override
|
|
public boolean commitCompletion(CompletionInfo text) {
|
|
Log.d(LOGTAG, "IME: commitCompletion");
|
|
return super.commitCompletion(text);
|
|
}
|
|
|
|
@Override
|
|
public boolean commitText(CharSequence text, int newCursorPosition) {
|
|
Log.d(LOGTAG, String.format("IME: commitText(\"%s\", %d)", text, newCursorPosition));
|
|
return super.commitText(text, newCursorPosition);
|
|
}
|
|
|
|
@Override
|
|
public boolean deleteSurroundingText(int leftLength, int rightLength) {
|
|
Log.d(LOGTAG, "IME: deleteSurroundingText(leftLen=" + leftLength +", rightLen=" + rightLength + ")");
|
|
return super.deleteSurroundingText(leftLength, rightLength);
|
|
}
|
|
|
|
@Override
|
|
public boolean finishComposingText() {
|
|
Log.d(LOGTAG, "IME: finishComposingText");
|
|
return super.finishComposingText();
|
|
}
|
|
|
|
@Override
|
|
public Editable getEditable() {
|
|
Log.d(LOGTAG, "IME: getEditable called from " + Thread.currentThread().getStackTrace()[0].toString());
|
|
return super.getEditable();
|
|
}
|
|
|
|
@Override
|
|
public boolean performContextMenuAction(int id) {
|
|
Log.d(LOGTAG, "IME: performContextMenuAction");
|
|
return super.performContextMenuAction(id);
|
|
}
|
|
|
|
@Override
|
|
public ExtractedText getExtractedText(ExtractedTextRequest req, int flags) {
|
|
Log.d(LOGTAG, "IME: getExtractedText");
|
|
ExtractedText extract = super.getExtractedText(req, flags);
|
|
if (extract != null)
|
|
Log.d(LOGTAG, String.format(". . . getExtractedText: extract.text=\"%s\", selStart=%d, selEnd=%d",
|
|
extract.text, extract.selectionStart, extract.selectionEnd));
|
|
return extract;
|
|
}
|
|
|
|
@Override
|
|
public CharSequence getTextAfterCursor(int length, int flags) {
|
|
Log.d(LOGTAG, "IME: getTextAfterCursor(length=" + length + ", flags=" + flags + ")");
|
|
CharSequence s = super.getTextAfterCursor(length, flags);
|
|
Log.d(LOGTAG, ". . . getTextAfterCursor returns \"" + s + "\"");
|
|
return s;
|
|
}
|
|
|
|
@Override
|
|
public CharSequence getTextBeforeCursor(int length, int flags) {
|
|
Log.d(LOGTAG, "IME: getTextBeforeCursor");
|
|
CharSequence s = super.getTextBeforeCursor(length, flags);
|
|
Log.d(LOGTAG, ". . . getTextBeforeCursor returns \"" + s + "\"");
|
|
return s;
|
|
}
|
|
|
|
@Override
|
|
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
|
Log.d(LOGTAG, String.format("IME: setComposingText(\"%s\", %d)", text, newCursorPosition));
|
|
return super.setComposingText(text, newCursorPosition);
|
|
}
|
|
|
|
@Override
|
|
public boolean setComposingRegion(int start, int end) {
|
|
Log.d(LOGTAG, "IME: setComposingRegion(start=" + start + ", end=" + end + ")");
|
|
return super.setComposingRegion(start, end);
|
|
}
|
|
|
|
@Override
|
|
public boolean setSelection(int start, int end) {
|
|
Log.d(LOGTAG, "IME: setSelection(start=" + start + ", end=" + end + ")");
|
|
return super.setSelection(start, end);
|
|
}
|
|
|
|
@Override
|
|
public String getComposingText() {
|
|
Log.d(LOGTAG, "IME: getComposingText");
|
|
String s = super.getComposingText();
|
|
Log.d(LOGTAG, ". . . getComposingText: Composing text = \"" + s + "\"");
|
|
return s;
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyDel() {
|
|
Log.d(LOGTAG, "IME: onKeyDel");
|
|
return super.onKeyDel();
|
|
}
|
|
|
|
@Override
|
|
public void notifyTextChange(InputMethodManager imm, String text,
|
|
int start, int oldEnd, int newEnd) {
|
|
Log.d(LOGTAG, String.format("IME: >notifyTextChange(\"%s\", start=%d, oldEnd=%d, newEnd=%d)",
|
|
text, start, oldEnd, newEnd));
|
|
super.notifyTextChange(imm, text, start, oldEnd, newEnd);
|
|
}
|
|
|
|
@Override
|
|
public void notifySelectionChange(InputMethodManager imm,
|
|
int start, int end) {
|
|
Log.d(LOGTAG, String.format("IME: >notifySelectionChange(start=%d, end=%d)", start, end));
|
|
super.notifySelectionChange(imm, start, end);
|
|
}
|
|
|
|
@Override
|
|
public void reset() {
|
|
Log.d(LOGTAG, "IME: reset");
|
|
super.reset();
|
|
}
|
|
|
|
@Override
|
|
public void onTextChanged(CharSequence s, int start, int before, int count)
|
|
{
|
|
Log.d(LOGTAG, String.format("IME: onTextChanged(\"%s\" start=%d, before=%d, count=%d)", s, start, before, count));
|
|
super.onTextChanged(s, start, before, count);
|
|
}
|
|
|
|
@Override
|
|
public void afterTextChanged(Editable s)
|
|
{
|
|
Log.d(LOGTAG, "IME: afterTextChanged(\"" + s + "\")");
|
|
super.afterTextChanged(s);
|
|
}
|
|
|
|
@Override
|
|
public void beforeTextChanged(CharSequence s, int start, int count, int after)
|
|
{
|
|
Log.d(LOGTAG, String.format("IME: beforeTextChanged(\"%s\", start=%d, count=%d, after=%d)", s, start, count, after));
|
|
super.beforeTextChanged(s, start, count, after);
|
|
}
|
|
|
|
@Override
|
|
public InputConnection onCreateInputConnection(EditorInfo outAttrs)
|
|
{
|
|
Log.d(LOGTAG, "IME: handleCreateInputConnection called");
|
|
return super.onCreateInputConnection(outAttrs);
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
|
Log.d(LOGTAG, "IME: onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
|
|
return super.onKeyPreIme(keyCode, event);
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
Log.d(LOGTAG, "IME: onKeyDown(keyCode=" + keyCode + ", event=" + event + ")");
|
|
return super.onKeyDown(keyCode, event);
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
|
Log.d(LOGTAG, "IME: onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
|
|
return super.onKeyUp(keyCode, event);
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
|
|
Log.d(LOGTAG, "IME: onKeyMultiple(keyCode=" + keyCode + ", repeatCount=" + repeatCount + ", event=" + event + ")");
|
|
return super.onKeyMultiple(keyCode, repeatCount, event);
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
|
|
Log.d(LOGTAG, "IME: onKeyLongPress(keyCode=" + keyCode + ", event=" + event + ")");
|
|
return super.onKeyLongPress(keyCode, event);
|
|
}
|
|
|
|
@Override
|
|
public void notifyIME(int type, int state) {
|
|
Log.d(LOGTAG, String.format("IME: >notifyIME(type=%d, state=%d)", type, state));
|
|
super.notifyIME(type, state);
|
|
}
|
|
|
|
@Override
|
|
public void notifyIMEChange(String text, int start, int end, int newEnd) {
|
|
Log.d(LOGTAG, String.format("IME: >notifyIMEChange(\"%s\", start=%d, end=%d, newEnd=%d)",
|
|
text, start, end, newEnd));
|
|
super.notifyIMEChange(text, start, end, newEnd);
|
|
}
|
|
}
|