зеркало из https://github.com/mozilla/pjs.git
597 строки
22 KiB
Java
597 строки
22 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>
|
|
*
|
|
* 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 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.util.*;
|
|
|
|
public class GeckoInputConnection
|
|
extends BaseInputConnection
|
|
implements TextWatcher
|
|
{
|
|
public GeckoInputConnection (View targetView) {
|
|
super(targetView, true);
|
|
mQueryResult = new SynchronousQueue<String>();
|
|
}
|
|
|
|
@Override
|
|
public boolean beginBatchEdit() {
|
|
//Log.d("GeckoAppJava", "IME: beginBatchEdit");
|
|
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean commitCompletion(CompletionInfo text) {
|
|
//Log.d("GeckoAppJava", "IME: commitCompletion");
|
|
|
|
return commitText(text.getText(), 1);
|
|
}
|
|
|
|
@Override
|
|
public boolean commitText(CharSequence text, int newCursorPosition) {
|
|
//Log.d("GeckoAppJava", "IME: commitText");
|
|
|
|
setComposingText(text, newCursorPosition);
|
|
finishComposingText();
|
|
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean deleteSurroundingText(int leftLength, int rightLength) {
|
|
//Log.d("GeckoAppJava", "IME: deleteSurroundingText");
|
|
|
|
/* deleteSurroundingText is supposed to ignore the composing text,
|
|
so we cancel any pending composition, delete the text, and then
|
|
restart the composition */
|
|
|
|
if (mComposing) {
|
|
// Cancel current composition
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(0, 0, 0, 0, 0, 0, null));
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_COMPOSITION_END, 0, 0));
|
|
}
|
|
|
|
// Select text to be deleted
|
|
int delStart, delLen;
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_GET_SELECTION, 0, 0));
|
|
try {
|
|
mQueryResult.take();
|
|
} catch (InterruptedException e) {
|
|
Log.e("GeckoAppJava", "IME: deleteSurroundingText interrupted", e);
|
|
return false;
|
|
}
|
|
delStart = mSelectionStart > leftLength ?
|
|
mSelectionStart - leftLength : 0;
|
|
delLen = mSelectionStart + rightLength - delStart;
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_SET_SELECTION, delStart, delLen));
|
|
|
|
// Restore composition / delete text
|
|
if (mComposing) {
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_COMPOSITION_BEGIN, 0, 0));
|
|
if (mComposingText.length() > 0) {
|
|
/* IME_SET_TEXT doesn't work well with empty strings */
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(0, mComposingText.length(),
|
|
GeckoEvent.IME_RANGE_RAWINPUT,
|
|
GeckoEvent.IME_RANGE_UNDERLINE, 0, 0,
|
|
mComposingText.toString()));
|
|
}
|
|
} else {
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_DELETE_TEXT, 0, 0));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean endBatchEdit() {
|
|
//Log.d("GeckoAppJava", "IME: endBatchEdit");
|
|
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean finishComposingText() {
|
|
//Log.d("GeckoAppJava", "IME: finishComposingText");
|
|
|
|
if (mComposing) {
|
|
// Set style to none
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(0, mComposingText.length(),
|
|
GeckoEvent.IME_RANGE_RAWINPUT, 0, 0, 0,
|
|
mComposingText));
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_COMPOSITION_END, 0, 0));
|
|
mComposing = false;
|
|
mComposingText = null;
|
|
|
|
// Make sure caret stays at the same position
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_SET_SELECTION,
|
|
mCompositionStart + mCompositionSelStart, 0));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public int getCursorCapsMode(int reqModes) {
|
|
//Log.d("GeckoAppJava", "IME: getCursorCapsMode");
|
|
|
|
return 0;
|
|
}
|
|
|
|
@Override
|
|
public Editable getEditable() {
|
|
Log.w("GeckoAppJava", "IME: getEditable called from " +
|
|
Thread.currentThread().getStackTrace()[0].toString());
|
|
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public boolean performContextMenuAction(int id) {
|
|
//Log.d("GeckoAppJava", "IME: performContextMenuAction");
|
|
|
|
// First we need to ask Gecko to tell us the full contents of the
|
|
// text field we're about to operate on.
|
|
String text;
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_GET_TEXT, 0, Integer.MAX_VALUE));
|
|
try {
|
|
text = mQueryResult.take();
|
|
} catch (InterruptedException e) {
|
|
Log.e("GeckoAppJava", "IME: performContextMenuAction interrupted", e);
|
|
return false;
|
|
}
|
|
|
|
switch (id) {
|
|
case R.id.selectAll:
|
|
setSelection(0, text.length());
|
|
break;
|
|
case R.id.cut:
|
|
// Fill the clipboard
|
|
GeckoAppShell.setClipboardText(text);
|
|
// If GET_TEXT returned an empty selection, we'll select everything
|
|
if (mSelectionLength <= 0)
|
|
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, we need to get the selection from Gecko
|
|
if (mSelectionLength > 0) {
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_GET_SELECTION, 0, 0));
|
|
try {
|
|
text = mQueryResult.take();
|
|
} catch (InterruptedException e) {
|
|
Log.e("GeckoAppJava", "IME: performContextMenuAction interrupted", e);
|
|
return false;
|
|
}
|
|
}
|
|
GeckoAppShell.setClipboardText(text);
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public ExtractedText getExtractedText(ExtractedTextRequest req, int flags) {
|
|
if (req == null)
|
|
return null;
|
|
|
|
//Log.d("GeckoAppJava", "IME: getExtractedText");
|
|
|
|
ExtractedText extract = new ExtractedText();
|
|
extract.flags = 0;
|
|
extract.partialStartOffset = -1;
|
|
extract.partialEndOffset = -1;
|
|
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_GET_SELECTION, 0, 0));
|
|
try {
|
|
mQueryResult.take();
|
|
} catch (InterruptedException e) {
|
|
Log.e("GeckoAppJava", "IME: getExtractedText interrupted", e);
|
|
return null;
|
|
}
|
|
extract.selectionStart = mSelectionStart;
|
|
extract.selectionEnd = mSelectionStart + mSelectionLength;
|
|
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_GET_TEXT, 0, Integer.MAX_VALUE));
|
|
try {
|
|
extract.startOffset = 0;
|
|
extract.text = mQueryResult.take();
|
|
|
|
if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0)
|
|
mUpdateRequest = req;
|
|
return extract;
|
|
|
|
} catch (InterruptedException e) {
|
|
Log.e("GeckoAppJava", "IME: getExtractedText interrupted", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public CharSequence getTextAfterCursor(int length, int flags) {
|
|
//Log.d("GeckoAppJava", "IME: getTextAfterCursor");
|
|
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_GET_SELECTION, 0, 0));
|
|
try {
|
|
mQueryResult.take();
|
|
} catch (InterruptedException e) {
|
|
Log.e("GeckoAppJava", "IME: getTextBefore/AfterCursor interrupted", e);
|
|
return null;
|
|
}
|
|
|
|
/* Compatible with both positive and negative length
|
|
(no need for separate code for getTextBeforeCursor) */
|
|
int textStart = mSelectionStart;
|
|
int textLength = length;
|
|
|
|
if (length < 0) {
|
|
textStart += length;
|
|
textLength = -length;
|
|
if (textStart < 0) {
|
|
textStart = 0;
|
|
textLength = mSelectionStart;
|
|
}
|
|
}
|
|
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_GET_TEXT, textStart, textLength));
|
|
try {
|
|
return mQueryResult.take();
|
|
} catch (InterruptedException e) {
|
|
Log.e("GeckoAppJava", "IME: getTextBefore/AfterCursor: Interrupted!", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public CharSequence getTextBeforeCursor(int length, int flags) {
|
|
//Log.d("GeckoAppJava", "IME: getTextBeforeCursor");
|
|
|
|
return getTextAfterCursor(-length, flags);
|
|
}
|
|
|
|
@Override
|
|
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
|
//Log.d("GeckoAppJava", "IME: setComposingText");
|
|
|
|
// Set new composing text
|
|
mComposingText = text != null ? text.toString() : "";
|
|
|
|
if (!mComposing) {
|
|
// Get current selection
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_GET_SELECTION, 0, 0));
|
|
try {
|
|
mQueryResult.take();
|
|
} catch (InterruptedException e) {
|
|
Log.e("GeckoAppJava", "IME: setComposingText interrupted", e);
|
|
return false;
|
|
}
|
|
// Make sure we are in a composition
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_COMPOSITION_BEGIN, 0, 0));
|
|
mComposing = true;
|
|
mCompositionStart = mSelectionLength >= 0 ?
|
|
mSelectionStart : mSelectionStart + mSelectionLength;
|
|
}
|
|
|
|
// Set new selection
|
|
// New selection should be within the composition
|
|
mCompositionSelStart = newCursorPosition > 0 ? mComposingText.length() : 0;
|
|
mCompositionSelLen = 0;
|
|
|
|
// 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);
|
|
|
|
// We need to count the selection as a transition
|
|
if (mCompositionSelLen >= 0) {
|
|
if (!pastSelStart && spanEnd >= mCompositionSelStart) {
|
|
spanEnd = mCompositionSelStart;
|
|
pastSelStart = true;
|
|
} else if (!pastSelEnd && spanEnd >=
|
|
mCompositionSelStart + mCompositionSelLen) {
|
|
spanEnd = mCompositionSelStart + mCompositionSelLen;
|
|
pastSelEnd = true;
|
|
rangeType = GeckoEvent.IME_RANGE_SELECTEDRAWTEXT;
|
|
}
|
|
} else {
|
|
if (!pastSelEnd && spanEnd >=
|
|
mCompositionSelStart + mCompositionSelLen) {
|
|
spanEnd = mCompositionSelStart + mCompositionSelLen;
|
|
pastSelEnd = true;
|
|
} else if (!pastSelStart &&
|
|
spanEnd >= mCompositionSelStart) {
|
|
spanEnd = mCompositionSelStart;
|
|
pastSelStart = true;
|
|
rangeType = GeckoEvent.IME_RANGE_SELECTEDRAWTEXT;
|
|
}
|
|
}
|
|
// 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
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(spanStart, spanEnd - spanStart,
|
|
rangeType, rangeStyles,
|
|
rangeForeColor, rangeBackColor));
|
|
|
|
spanStart = spanEnd;
|
|
} while (spanStart < text.length());
|
|
} else {
|
|
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)
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(mCompositionSelStart + mCompositionSelLen, 0,
|
|
GeckoEvent.IME_RANGE_CARETPOSITION, 0, 0, 0,
|
|
mComposingText));
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean setSelection(int start, int end) {
|
|
//Log.d("GeckoAppJava", "IME: setSelection");
|
|
|
|
if (mComposing) {
|
|
/* Translate to fake selection positions */
|
|
start -= mCompositionStart;
|
|
end -= mCompositionStart;
|
|
|
|
if (start < 0)
|
|
start = 0;
|
|
else if (start > mComposingText.length())
|
|
start = mComposingText.length();
|
|
|
|
if (end < 0)
|
|
end = 0;
|
|
else if (end > mComposingText.length())
|
|
end = mComposingText.length();
|
|
|
|
mCompositionSelStart = start;
|
|
mCompositionSelLen = end - start;
|
|
} else {
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_SET_SELECTION,
|
|
start, end - start));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
|
|
if (mComposingText.length() > 0) {
|
|
mComposingText = mComposingText.substring(0,
|
|
mComposingText.length() - 1);
|
|
if (mComposingText.length() > 0)
|
|
return false;
|
|
}
|
|
|
|
commitText(null, 1);
|
|
return true;
|
|
}
|
|
|
|
public void notifyTextChange(InputMethodManager imm, String text,
|
|
int start, int oldEnd, int newEnd) {
|
|
//Log.d("GeckoAppJava", "IME: notifyTextChange");
|
|
|
|
if (!text.contentEquals(GeckoApp.surfaceView.mEditable))
|
|
GeckoApp.surfaceView.setEditable(text);
|
|
|
|
if (mUpdateRequest == 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(GeckoApp.surfaceView,
|
|
mUpdateRequest.token, mUpdateExtract);
|
|
}
|
|
|
|
public void notifySelectionChange(InputMethodManager imm,
|
|
int start, int end) {
|
|
//Log.d("GeckoAppJava", "IME: notifySelectionChange");
|
|
|
|
if (mComposing)
|
|
imm.updateSelection(GeckoApp.surfaceView,
|
|
mCompositionStart + mCompositionSelStart,
|
|
mCompositionStart + mCompositionSelStart + mCompositionSelLen,
|
|
mCompositionStart,
|
|
mCompositionStart + mComposingText.length());
|
|
else
|
|
imm.updateSelection(GeckoApp.surfaceView, start, end, -1, -1);
|
|
|
|
int maxLen = GeckoApp.surfaceView.mEditable.length();
|
|
Selection.setSelection(GeckoApp.surfaceView.mEditable,
|
|
Math.min(start, maxLen),
|
|
Math.min(end, maxLen));
|
|
}
|
|
|
|
public void reset() {
|
|
mComposing = false;
|
|
mComposingText = null;
|
|
mUpdateRequest = null;
|
|
}
|
|
|
|
// TextWatcher
|
|
public void onTextChanged(CharSequence s, int start, int before, int count)
|
|
{
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_SET_SELECTION, start, before));
|
|
|
|
if (count == 0) {
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_DELETE_TEXT, 0, 0));
|
|
} else {
|
|
// Start and stop composition to force UI updates.
|
|
finishComposingText();
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_COMPOSITION_BEGIN, 0, 0));
|
|
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(0, count,
|
|
GeckoEvent.IME_RANGE_RAWINPUT, 0, 0, 0,
|
|
s.subSequence(start, start + count).toString()));
|
|
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_COMPOSITION_END, 0, 0));
|
|
|
|
GeckoAppShell.sendEventToGecko(
|
|
new GeckoEvent(GeckoEvent.IME_SET_SELECTION, start + count, 0));
|
|
|
|
|
|
}
|
|
}
|
|
|
|
public void afterTextChanged(Editable s)
|
|
{
|
|
}
|
|
|
|
public void beforeTextChanged(CharSequence s, int start, int count, int after)
|
|
{
|
|
}
|
|
|
|
// Is a composition active?
|
|
boolean mComposing;
|
|
// Composition text when a composition is active
|
|
String mComposingText;
|
|
// Start index of the composition within the text body
|
|
int mCompositionStart;
|
|
/* During a composition, we should not alter the real selection,
|
|
therefore we keep our own offsets to emulate selection */
|
|
// Start of fake selection, relative to start of composition
|
|
int mCompositionSelStart;
|
|
// Length of fake selection
|
|
int mCompositionSelLen;
|
|
|
|
ExtractedTextRequest mUpdateRequest;
|
|
final ExtractedText mUpdateExtract = new ExtractedText();
|
|
|
|
int mSelectionStart, mSelectionLength;
|
|
SynchronousQueue<String> mQueryResult;
|
|
}
|
|
|