mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-01 06:35:42 +00:00
374 lines
13 KiB
Java
374 lines
13 KiB
Java
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
package org.mozilla.gecko.toolbar;
|
|
|
|
import org.mozilla.gecko.R;
|
|
import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener;
|
|
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.Rect;
|
|
import android.text.Editable;
|
|
import android.text.InputType;
|
|
import android.text.Spanned;
|
|
import android.text.TextUtils;
|
|
import android.text.TextWatcher;
|
|
import android.util.AttributeSet;
|
|
import android.util.Log;
|
|
import android.view.KeyEvent;
|
|
import android.view.View;
|
|
import android.view.View.OnKeyListener;
|
|
import android.view.inputmethod.EditorInfo;
|
|
import android.view.inputmethod.InputMethodManager;
|
|
|
|
/**
|
|
* {@code ToolbarEditText} is the text entry used when the toolbar
|
|
* is in edit state. It handles all the necessary input method machinery
|
|
* as well as the tracking of different text types (empty, search, or url).
|
|
* It's meant to be owned by {@code ToolbarEditLayout}.
|
|
*/
|
|
public class ToolbarEditText extends CustomEditText
|
|
implements AutocompleteHandler {
|
|
|
|
private static final String LOGTAG = "GeckoToolbarEditText";
|
|
|
|
// Used to track the current type of content in the
|
|
// text entry so that ToolbarEditLayout can update its
|
|
// state accordingly.
|
|
enum TextType {
|
|
EMPTY,
|
|
SEARCH_QUERY,
|
|
URL
|
|
}
|
|
|
|
interface OnTextTypeChangeListener {
|
|
public void onTextTypeChange(ToolbarEditText editText, TextType textType);
|
|
}
|
|
|
|
private final Context mContext;
|
|
|
|
// Type of the URL bar go/search button
|
|
private TextType mToolbarTextType;
|
|
// Type of the keyboard go/search button (cannot be EMPTY)
|
|
private TextType mKeyboardTextType;
|
|
|
|
private OnCommitListener mCommitListener;
|
|
private OnDismissListener mDismissListener;
|
|
private OnFilterListener mFilterListener;
|
|
private OnTextTypeChangeListener mTextTypeListener;
|
|
|
|
// The previous autocomplete result returned to us
|
|
private String mAutoCompleteResult = "";
|
|
|
|
// The user typed part of the autocomplete result
|
|
private String mAutoCompletePrefix = null;
|
|
|
|
private boolean mDelayRestartInput;
|
|
|
|
public ToolbarEditText(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
mContext = context;
|
|
|
|
mToolbarTextType = TextType.EMPTY;
|
|
mKeyboardTextType = TextType.URL;
|
|
}
|
|
|
|
void setOnCommitListener(OnCommitListener listener) {
|
|
mCommitListener = listener;
|
|
}
|
|
|
|
void setOnDismissListener(OnDismissListener listener) {
|
|
mDismissListener = listener;
|
|
}
|
|
|
|
void setOnFilterListener(OnFilterListener listener) {
|
|
mFilterListener = listener;
|
|
}
|
|
|
|
void setOnTextTypeChangeListener(OnTextTypeChangeListener listener) {
|
|
mTextTypeListener = listener;
|
|
}
|
|
|
|
@Override
|
|
public void onAttachedToWindow() {
|
|
setOnKeyListener(new KeyListener());
|
|
setOnKeyPreImeListener(new KeyPreImeListener());
|
|
addTextChangedListener(new TextChangeListener());
|
|
}
|
|
|
|
@Override
|
|
public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
|
|
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
|
|
|
|
if (gainFocus) {
|
|
return;
|
|
}
|
|
|
|
InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
try {
|
|
imm.hideSoftInputFromWindow(getWindowToken(), 0);
|
|
} catch (NullPointerException e) {
|
|
Log.e(LOGTAG, "InputMethodManagerService, why are you throwing"
|
|
+ " a NullPointerException? See bug 782096", e);
|
|
}
|
|
}
|
|
|
|
// Return early if we're backspacing through the string, or
|
|
// have no autocomplete results
|
|
@Override
|
|
public final void onAutocomplete(final String result) {
|
|
if (!isEnabled()) {
|
|
return;
|
|
}
|
|
|
|
final String text = getText().toString();
|
|
|
|
if (result == null) {
|
|
mAutoCompleteResult = "";
|
|
return;
|
|
}
|
|
|
|
if (!result.startsWith(text) || text.equals(result)) {
|
|
return;
|
|
}
|
|
|
|
mAutoCompleteResult = result;
|
|
getText().append(result.substring(text.length()));
|
|
setSelection(text.length(), result.length());
|
|
}
|
|
|
|
@Override
|
|
public void setEnabled(boolean enabled) {
|
|
super.setEnabled(enabled);
|
|
updateTextTypeFromText(getText().toString());
|
|
}
|
|
|
|
private void updateKeyboardInputType() {
|
|
// If the user enters a space, then we know they are entering
|
|
// search terms, not a URL. We can then switch to text mode so,
|
|
// 1) the IME auto-inserts spaces between words
|
|
// 2) the IME doesn't reset input keyboard to Latin keyboard.
|
|
final String text = getText().toString();
|
|
final int currentInputType = getInputType();
|
|
|
|
final int newInputType = StringUtils.isSearchQuery(text, false)
|
|
? (currentInputType & ~InputType.TYPE_TEXT_VARIATION_URI) // Text mode
|
|
: (currentInputType | InputType.TYPE_TEXT_VARIATION_URI); // URL mode
|
|
|
|
if (newInputType != currentInputType) {
|
|
setRawInputType(newInputType);
|
|
}
|
|
}
|
|
|
|
private static boolean hasCompositionString(Editable content) {
|
|
Object[] spans = content.getSpans(0, content.length(), Object.class);
|
|
|
|
if (spans != null) {
|
|
for (Object span : spans) {
|
|
if ((content.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
|
|
// Found composition string.
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void setTextType(TextType textType) {
|
|
mToolbarTextType = textType;
|
|
|
|
if (textType != TextType.EMPTY) {
|
|
mKeyboardTextType = textType;
|
|
}
|
|
if (mTextTypeListener != null) {
|
|
mTextTypeListener.onTextTypeChange(this, textType);
|
|
}
|
|
}
|
|
|
|
private void updateTextTypeFromText(String text) {
|
|
if (text.length() == 0) {
|
|
setTextType(TextType.EMPTY);
|
|
return;
|
|
}
|
|
|
|
if (InputMethods.shouldDisableUrlBarUpdate(mContext)) {
|
|
// Set button type to match the previous keyboard type
|
|
setTextType(mKeyboardTextType);
|
|
return;
|
|
}
|
|
|
|
final int actionBits = getImeOptions() & EditorInfo.IME_MASK_ACTION;
|
|
|
|
final int imeAction;
|
|
if (StringUtils.isSearchQuery(text, actionBits == EditorInfo.IME_ACTION_SEARCH)) {
|
|
imeAction = EditorInfo.IME_ACTION_SEARCH;
|
|
} else {
|
|
imeAction = EditorInfo.IME_ACTION_GO;
|
|
}
|
|
|
|
InputMethodManager imm = InputMethods.getInputMethodManager(mContext);
|
|
if (imm == null) {
|
|
return;
|
|
}
|
|
|
|
boolean restartInput = false;
|
|
if (actionBits != imeAction) {
|
|
int optionBits = getImeOptions() & ~EditorInfo.IME_MASK_ACTION;
|
|
setImeOptions(optionBits | imeAction);
|
|
|
|
mDelayRestartInput = (imeAction == EditorInfo.IME_ACTION_GO) &&
|
|
(InputMethods.shouldDelayUrlBarUpdate(mContext));
|
|
|
|
if (!mDelayRestartInput) {
|
|
restartInput = true;
|
|
}
|
|
} else if (mDelayRestartInput) {
|
|
// Only call delayed restartInput when actionBits == imeAction
|
|
// so if there are two restarts in a row, the first restarts will
|
|
// be discarded and the second restart will be properly delayed
|
|
mDelayRestartInput = false;
|
|
restartInput = true;
|
|
}
|
|
|
|
if (!restartInput) {
|
|
// If the text content was previously empty, the toolbar text type
|
|
// is empty as well. Since the keyboard text type cannot be empty,
|
|
// the two text types are now inconsistent. Reset the toolbar text
|
|
// type here to the keyboard text type to ensure consistency.
|
|
setTextType(mKeyboardTextType);
|
|
return;
|
|
}
|
|
updateKeyboardInputType();
|
|
imm.restartInput(ToolbarEditText.this);
|
|
|
|
setTextType(imeAction == EditorInfo.IME_ACTION_GO ?
|
|
TextType.URL : TextType.SEARCH_QUERY);
|
|
}
|
|
|
|
private class TextChangeListener implements TextWatcher {
|
|
@Override
|
|
public void afterTextChanged(final Editable s) {
|
|
if (!isEnabled()) {
|
|
return;
|
|
}
|
|
|
|
final String text = s.toString();
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
mAutoCompletePrefix = text;
|
|
|
|
if (reuseAutocomplete) {
|
|
onAutocomplete(mAutoCompleteResult);
|
|
}
|
|
}
|
|
|
|
// If the edit text has a composition string, don't call updateGoButton().
|
|
// That method resets IME and composition state will be broken.
|
|
if (!hasCompositionString(s) || InputMethods.isGestureKeyboard(mContext)) {
|
|
updateTextTypeFromText(text);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void beforeTextChanged(CharSequence s, int start, int count,
|
|
int after) {
|
|
// do nothing
|
|
}
|
|
|
|
@Override
|
|
public void onTextChanged(CharSequence s, int start, int before,
|
|
int count) {
|
|
// do nothing
|
|
}
|
|
}
|
|
|
|
private class KeyPreImeListener implements OnKeyPreImeListener {
|
|
@Override
|
|
public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) {
|
|
// We only want to process one event per tap
|
|
if (event.getAction() != KeyEvent.ACTION_DOWN) {
|
|
return false;
|
|
}
|
|
|
|
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
|
// If the edit text has a composition string, don't submit the text yet.
|
|
// ENTER is needed to commit the composition string.
|
|
final Editable content = getText();
|
|
if (!hasCompositionString(content)) {
|
|
if (mCommitListener != null) {
|
|
mCommitListener.onCommit();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
|
// Drop the virtual keyboard.
|
|
clearFocus();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private class KeyListener implements View.OnKeyListener {
|
|
@Override
|
|
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
|
if (keyCode == KeyEvent.KEYCODE_ENTER || GamepadUtils.isActionKey(event)) {
|
|
if (event.getAction() != KeyEvent.ACTION_DOWN) {
|
|
return true;
|
|
}
|
|
|
|
if (mCommitListener != null) {
|
|
mCommitListener.onCommit();
|
|
}
|
|
|
|
return true;
|
|
} else if (GamepadUtils.isBackKey(event)) {
|
|
if (mDismissListener != null) {
|
|
mDismissListener.onDismiss();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|