зеркало из https://github.com/mozilla/gecko-dev.git
Bug 994472 - Use text spans to implement autocompletion; r=wesj
This commit is contained in:
Родитель
a24944a5fb
Коммит
d479138380
|
@ -11,15 +11,20 @@ import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
|
|||
import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
|
||||
import org.mozilla.gecko.CustomEditText;
|
||||
import org.mozilla.gecko.CustomEditText.OnKeyPreImeListener;
|
||||
import org.mozilla.gecko.InputMethods;
|
||||
import org.mozilla.gecko.util.GamepadUtils;
|
||||
import org.mozilla.gecko.util.StringUtils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
import android.text.Editable;
|
||||
import android.text.NoCopySpan;
|
||||
import android.text.Selection;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
|
@ -36,6 +41,7 @@ public class ToolbarEditText extends CustomEditText
|
|||
implements AutocompleteHandler {
|
||||
|
||||
private static final String LOGTAG = "GeckoToolbarEditText";
|
||||
private static final NoCopySpan AUTOCOMPLETE_SPAN = new NoCopySpan.Concrete();
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
|
@ -45,9 +51,12 @@ public class ToolbarEditText extends CustomEditText
|
|||
|
||||
// The previous autocomplete result returned to us
|
||||
private String mAutoCompleteResult = "";
|
||||
|
||||
// The user typed part of the autocomplete result
|
||||
private String mAutoCompletePrefix = null;
|
||||
// Length of the user-typed portion of the result
|
||||
private int mAutoCompletePrefixLength;
|
||||
// If text change is due to us setting autocomplete
|
||||
private boolean mSettingAutoComplete;
|
||||
// Spans used for marking the autocomplete text
|
||||
private Object[] mAutoCompleteSpans;
|
||||
|
||||
public ToolbarEditText(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
@ -70,6 +79,7 @@ public class ToolbarEditText extends CustomEditText
|
|||
public void onAttachedToWindow() {
|
||||
setOnKeyListener(new KeyListener());
|
||||
setOnKeyPreImeListener(new KeyPreImeListener());
|
||||
setOnSelectionChangedListener(new SelectionChangeListener());
|
||||
addTextChangedListener(new TextChangeListener());
|
||||
}
|
||||
|
||||
|
@ -82,8 +92,9 @@ public class ToolbarEditText extends CustomEditText
|
|||
return;
|
||||
}
|
||||
|
||||
InputMethodManager imm = (InputMethodManager)
|
||||
mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
removeAutocomplete(getText());
|
||||
|
||||
final InputMethodManager imm = InputMethods.getInputMethodManager(mContext);
|
||||
try {
|
||||
imm.restartInput(this);
|
||||
imm.hideSoftInputFromWindow(getWindowToken(), 0);
|
||||
|
@ -93,33 +104,202 @@ public class ToolbarEditText extends CustomEditText
|
|||
}
|
||||
}
|
||||
|
||||
// Return early if we're backspacing through the string, or
|
||||
// have no autocomplete results
|
||||
@Override
|
||||
public final void onAutocomplete(final String result) {
|
||||
if (!isEnabled()) {
|
||||
/**
|
||||
* Mark the start of autocomplete changes so our text change
|
||||
* listener does not react to changes in autocomplete text
|
||||
*/
|
||||
private void beginSettingAutocomplete() {
|
||||
beginBatchEdit();
|
||||
mSettingAutoComplete = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the end of autocomplete changes
|
||||
*/
|
||||
private void endSettingAutocomplete() {
|
||||
mSettingAutoComplete = false;
|
||||
endBatchEdit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset autocomplete states to their initial values
|
||||
*/
|
||||
private void resetAutocompleteState() {
|
||||
final int textColor = getCurrentTextColor();
|
||||
|
||||
mAutoCompleteSpans = new Object[] {
|
||||
// Span to mark the autocomplete text
|
||||
AUTOCOMPLETE_SPAN,
|
||||
// Span to change the autocomplete text color
|
||||
new ForegroundColorSpan(Color.argb(
|
||||
0x80, Color.red(textColor), Color.green(textColor), Color.blue(textColor)))
|
||||
};
|
||||
|
||||
mAutoCompleteResult = "";
|
||||
mAutoCompletePrefixLength = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the portion of text that is not marked as autocomplete text.
|
||||
*
|
||||
* @param text Current text content that may include autocomplete text
|
||||
*/
|
||||
private static String getNonAutocompleteText(final Editable text) {
|
||||
final int start = text.getSpanStart(AUTOCOMPLETE_SPAN);
|
||||
if (start < 0) {
|
||||
// No autocomplete text; return the whole string.
|
||||
return text.toString();
|
||||
}
|
||||
|
||||
// Only return the portion that's not autocomplete text
|
||||
return TextUtils.substring(text, 0, start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any autocomplete text
|
||||
*
|
||||
* @param text Current text content that may include autocomplete text
|
||||
*/
|
||||
private void removeAutocomplete(final Editable text) {
|
||||
final int start = text.getSpanStart(AUTOCOMPLETE_SPAN);
|
||||
if (start < 0) {
|
||||
// No autocomplete text
|
||||
return;
|
||||
}
|
||||
|
||||
final String text = getText().toString();
|
||||
beginSettingAutocomplete();
|
||||
|
||||
if (result == null) {
|
||||
// When we call delete() here, the autocomplete spans we set are removed as well.
|
||||
text.delete(start, text.length());
|
||||
|
||||
// Keep mAutoCompletePrefixLength the same because the prefix has not changed.
|
||||
// Clear mAutoCompleteResult to make sure we get fresh autocomplete text next time.
|
||||
mAutoCompleteResult = "";
|
||||
|
||||
endSettingAutocomplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert any autocomplete text to regular text
|
||||
*
|
||||
* @param text Current text content that may include autocomplete text
|
||||
*/
|
||||
private void commitAutocomplete(final Editable text) {
|
||||
beginSettingAutocomplete();
|
||||
|
||||
// Remove all spans here to convert from autocomplete text to regular text
|
||||
for (final Object span : mAutoCompleteSpans) {
|
||||
text.removeSpan(span);
|
||||
}
|
||||
|
||||
// Keep mAutoCompleteResult the same because the result has not changed.
|
||||
// Reset mAutoCompletePrefixLength because the prefix now includes the autocomplete text.
|
||||
mAutoCompletePrefixLength = text.length();
|
||||
|
||||
endSettingAutocomplete();
|
||||
|
||||
// Filter on the new text
|
||||
if (mFilterListener != null) {
|
||||
mFilterListener.onFilter(text.toString(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add autocomplete text based on the result URI.
|
||||
*
|
||||
* @param result Result URI to be turned into autocomplete text
|
||||
*/
|
||||
@Override
|
||||
public final void onAutocomplete(final String result) {
|
||||
if (!isEnabled() || result == null) {
|
||||
mAutoCompleteResult = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.startsWith(text) || text.equals(result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Editable text = getText();
|
||||
final int textLength = text.length();
|
||||
final int resultLength = result.length();
|
||||
final int autoCompleteStart = text.getSpanStart(AUTOCOMPLETE_SPAN);
|
||||
mAutoCompleteResult = result;
|
||||
getText().append(result.substring(text.length()));
|
||||
setSelection(text.length(), result.length());
|
||||
}
|
||||
|
||||
private void resetAutocompleteState() {
|
||||
mAutoCompleteResult = "";
|
||||
mAutoCompletePrefix = null;
|
||||
if (autoCompleteStart > -1) {
|
||||
// Autocomplete text already exists; we should replace existing autocomplete text.
|
||||
|
||||
// If the result and the current text don't have the same prefixes,
|
||||
// the result is stale and we should wait for the another result to come in.
|
||||
if (!TextUtils.regionMatches(result, 0, text, 0, autoCompleteStart)) {
|
||||
return;
|
||||
}
|
||||
|
||||
beginSettingAutocomplete();
|
||||
|
||||
// Replace the existing autocomplete text with new one.
|
||||
// replace() preserves the autocomplete spans that we set before.
|
||||
text.replace(autoCompleteStart, textLength, result, autoCompleteStart, resultLength);
|
||||
|
||||
endSettingAutocomplete();
|
||||
|
||||
} else {
|
||||
// No autocomplete text yet; we should add autocomplete text
|
||||
|
||||
// If the result prefix doesn't match the current text,
|
||||
// the result is stale and we should wait for the another result to come in.
|
||||
if (resultLength <= textLength ||
|
||||
!TextUtils.regionMatches(result, 0, text, 0, textLength)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Object[] spans = text.getSpans(textLength, textLength, Object.class);
|
||||
final int[] spanStarts = new int[spans.length];
|
||||
final int[] spanEnds = new int[spans.length];
|
||||
final int[] spanFlags = new int[spans.length];
|
||||
|
||||
// Save selection/composing span bounds so we can restore them later.
|
||||
for (int i = 0; i < spans.length; i++) {
|
||||
final Object span = spans[i];
|
||||
final int spanFlag = text.getSpanFlags(span);
|
||||
|
||||
// We don't care about spans that are not selection or composing spans.
|
||||
// For those spans, spanFlag[i] will be 0 and we don't restore them.
|
||||
if ((spanFlag & Spanned.SPAN_COMPOSING) == 0 &&
|
||||
(span != Selection.SELECTION_START) &&
|
||||
(span != Selection.SELECTION_END)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
spanStarts[i] = text.getSpanStart(span);
|
||||
spanEnds[i] = text.getSpanEnd(span);
|
||||
spanFlags[i] = spanFlag;
|
||||
}
|
||||
|
||||
beginSettingAutocomplete();
|
||||
|
||||
// First add trailing text.
|
||||
text.append(result, textLength, resultLength);
|
||||
|
||||
// Mark added text as autocomplete text.
|
||||
for (final Object span : mAutoCompleteSpans) {
|
||||
text.setSpan(span, textLength, resultLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
// Make sure the autocomplete text is visible. If the autocomplete text is too
|
||||
// long, it would appear the cursor will be scrolled out of view. However, this
|
||||
// is not the case in practice, because EditText still makes sure the cursor is
|
||||
// still in view.
|
||||
bringPointIntoView(resultLength);
|
||||
|
||||
// Restore selection/composing spans.
|
||||
for (int i = 0; i < spans.length; i++) {
|
||||
final int spanFlag = spanFlags[i];
|
||||
if (spanFlag == 0) {
|
||||
// Skip if the span was ignored before.
|
||||
continue;
|
||||
}
|
||||
text.setSpan(spans[i], spanStarts[i], spanEnds[i], spanFlag);
|
||||
}
|
||||
|
||||
endSettingAutocomplete();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasCompositionString(Editable content) {
|
||||
|
@ -137,44 +317,64 @@ public class ToolbarEditText extends CustomEditText
|
|||
return false;
|
||||
}
|
||||
|
||||
private class TextChangeListener implements TextWatcher {
|
||||
private class SelectionChangeListener implements OnSelectionChangedListener {
|
||||
@Override
|
||||
public void afterTextChanged(final Editable s) {
|
||||
if (!isEnabled()) {
|
||||
public void onSelectionChanged(final int selStart, final int selEnd) {
|
||||
// The user has repositioned the cursor somewhere. We need to adjust
|
||||
// the autocomplete text depending on where the new cursor is.
|
||||
|
||||
final Editable text = getText();
|
||||
final int start = text.getSpanStart(AUTOCOMPLETE_SPAN);
|
||||
|
||||
if (start < 0 || (start == selStart && start == selEnd)) {
|
||||
// Do not commit autocomplete text if there is no autocomplete text
|
||||
// or if selection is still at start of autocomplete text
|
||||
return;
|
||||
}
|
||||
|
||||
final String text = s.toString();
|
||||
if (selStart <= start && selEnd <= start) {
|
||||
// The cursor is in user-typed text; remove any autocomplete text.
|
||||
removeAutocomplete(text);
|
||||
} else {
|
||||
// The cursor is in the autocomplete text; commit it so it becomes regular text.
|
||||
commitAutocomplete(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean useHandler = false;
|
||||
boolean reuseAutocomplete = false;
|
||||
|
||||
if (!hasCompositionString(s) && !StringUtils.isSearchQuery(text, false)) {
|
||||
useHandler = true;
|
||||
|
||||
// If you're hitting backspace (the string is getting smaller
|
||||
// or is unchanged), don't autocomplete.
|
||||
if (mAutoCompletePrefix != null && (mAutoCompletePrefix.length() >= text.length())) {
|
||||
useHandler = false;
|
||||
} else if (mAutoCompleteResult != null && mAutoCompleteResult.startsWith(text)) {
|
||||
// If this text already matches our autocomplete text, autocomplete likely
|
||||
// won't change. Just reuse the old autocomplete value.
|
||||
useHandler = false;
|
||||
reuseAutocomplete = true;
|
||||
}
|
||||
private class TextChangeListener implements TextWatcher {
|
||||
@Override
|
||||
public void afterTextChanged(final Editable editable) {
|
||||
if (!isEnabled() || mSettingAutoComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is the autocomplete text being set, don't run the filter.
|
||||
if (TextUtils.isEmpty(mAutoCompleteResult) || !mAutoCompleteResult.equals(text)) {
|
||||
if (mFilterListener != null) {
|
||||
mFilterListener.onFilter(text, useHandler ? ToolbarEditText.this : null);
|
||||
}
|
||||
final String text = getNonAutocompleteText(editable);
|
||||
final int textLength = text.length();
|
||||
boolean doAutocomplete = true;
|
||||
|
||||
mAutoCompletePrefix = text;
|
||||
if (StringUtils.isSearchQuery(text, false)) {
|
||||
doAutocomplete = false;
|
||||
} else if (mAutoCompletePrefixLength > textLength) {
|
||||
// If you're hitting backspace (the string is getting smaller), don't autocomplete
|
||||
doAutocomplete = false;
|
||||
}
|
||||
|
||||
if (reuseAutocomplete) {
|
||||
onAutocomplete(mAutoCompleteResult);
|
||||
}
|
||||
mAutoCompletePrefixLength = textLength;
|
||||
|
||||
if (doAutocomplete && mAutoCompleteResult.startsWith(text)) {
|
||||
// If this text already matches our autocomplete text, autocomplete likely
|
||||
// won't change. Just reuse the old autocomplete value.
|
||||
onAutocomplete(mAutoCompleteResult);
|
||||
doAutocomplete = false;
|
||||
} else {
|
||||
// Otherwise, remove the old autocomplete text
|
||||
// until any new autocomplete text gets added.
|
||||
removeAutocomplete(editable);
|
||||
}
|
||||
|
||||
if (mFilterListener != null) {
|
||||
mFilterListener.onFilter(text, doAutocomplete ? ToolbarEditText.this : null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче