/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.webkit; import android.annotation.SystemApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.text.Editable; import android.text.Selection; import android.text.Spannable; import android.text.TextWatcher; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.TextView; /** * @hide */ @SystemApi public class FindActionModeCallback implements ActionMode.Callback, TextWatcher, View.OnClickListener, WebView.FindListener { private View mCustomView; private EditText mEditText; private TextView mMatches; private WebView mWebView; private InputMethodManager mInput; private Resources mResources; private boolean mMatchesFound; private int mNumberOfMatches; private int mActiveMatchIndex; private ActionMode mActionMode; public FindActionModeCallback(Context context) { mCustomView = LayoutInflater.from(context).inflate( com.android.internal.R.layout.webview_find, null); mEditText = (EditText) mCustomView.findViewById( com.android.internal.R.id.edit); mEditText.setCustomSelectionActionModeCallback(new NoAction()); mEditText.setOnClickListener(this); setText(""); mMatches = (TextView) mCustomView.findViewById( com.android.internal.R.id.matches); mInput = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); mResources = context.getResources(); } public void finish() { mActionMode.finish(); } /* * Place text in the text field so it can be searched for. Need to press * the find next or find previous button to find all of the matches. */ public void setText(String text) { mEditText.setText(text); Spannable span = (Spannable) mEditText.getText(); int length = span.length(); // Ideally, we would like to set the selection to the whole field, // but this brings up the Text selection CAB, which dismisses this // one. Selection.setSelection(span, length, length); // Necessary each time we set the text, so that this will watch // changes to it. span.setSpan(this, 0, length, Spannable.SPAN_INCLUSIVE_INCLUSIVE); mMatchesFound = false; } /* * Set the WebView to search. Must be non null. */ public void setWebView(WebView webView) { if (null == webView) { throw new AssertionError("WebView supplied to " + "FindActionModeCallback cannot be null"); } mWebView = webView; mWebView.setFindDialogFindListener(this); } @Override public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) { if (isDoneCounting) { updateMatchCount(activeMatchOrdinal, numberOfMatches, numberOfMatches == 0); } } /* * Move the highlight to the next match. * @param next If true, find the next match further down in the document. * If false, find the previous match, up in the document. */ private void findNext(boolean next) { if (mWebView == null) { throw new AssertionError( "No WebView for FindActionModeCallback::findNext"); } if (!mMatchesFound) { findAll(); return; } if (0 == mNumberOfMatches) { // There are no matches, so moving to the next match will not do // anything. return; } mWebView.findNext(next); updateMatchesString(); } /* * Highlight all the instances of the string from mEditText in mWebView. */ public void findAll() { if (mWebView == null) { throw new AssertionError( "No WebView for FindActionModeCallback::findAll"); } CharSequence find = mEditText.getText(); if (0 == find.length()) { mWebView.clearMatches(); mMatches.setVisibility(View.GONE); mMatchesFound = false; mWebView.findAll(null); } else { mMatchesFound = true; mMatches.setVisibility(View.INVISIBLE); mNumberOfMatches = 0; mWebView.findAllAsync(find.toString()); } } public void showSoftInput() { mInput.startGettingWindowFocus(mEditText.getRootView()); mInput.focusIn(mEditText); mInput.showSoftInput(mEditText, 0); } public void updateMatchCount(int matchIndex, int matchCount, boolean isEmptyFind) { if (!isEmptyFind) { mNumberOfMatches = matchCount; mActiveMatchIndex = matchIndex; updateMatchesString(); } else { mMatches.setVisibility(View.GONE); mNumberOfMatches = 0; } } /* * Update the string which tells the user how many matches were found, and * which match is currently highlighted. */ private void updateMatchesString() { if (mNumberOfMatches == 0) { mMatches.setText(com.android.internal.R.string.no_matches); } else { mMatches.setText(mResources.getQuantityString( com.android.internal.R.plurals.matches_found, mNumberOfMatches, mActiveMatchIndex + 1, mNumberOfMatches)); } mMatches.setVisibility(View.VISIBLE); } // OnClickListener implementation @Override public void onClick(View v) { findNext(true); } // ActionMode.Callback implementation @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { if (!mode.isUiFocusable()) { // If the action mode we're running in is not focusable the user // will not be able to type into the find on page field. This // should only come up when we're running in a dialog which is // already less than ideal; disable the option for now. return false; } mode.setCustomView(mCustomView); mode.getMenuInflater().inflate(com.android.internal.R.menu.webview_find, menu); mActionMode = mode; Editable edit = mEditText.getText(); Selection.setSelection(edit, edit.length()); mMatches.setVisibility(View.GONE); mMatchesFound = false; mMatches.setText("0"); mEditText.requestFocus(); return true; } @Override public void onDestroyActionMode(ActionMode mode) { mActionMode = null; mWebView.notifyFindDialogDismissed(); mWebView.setFindDialogFindListener(null); mInput.hideSoftInputFromWindow(mWebView.getWindowToken(), 0); } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { if (mWebView == null) { throw new AssertionError( "No WebView for FindActionModeCallback::onActionItemClicked"); } mInput.hideSoftInputFromWindow(mWebView.getWindowToken(), 0); switch(item.getItemId()) { case com.android.internal.R.id.find_prev: findNext(false); break; case com.android.internal.R.id.find_next: findNext(true); break; default: return false; } return true; } // TextWatcher implementation @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { // Does nothing. Needed to implement TextWatcher. } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { findAll(); } @Override public void afterTextChanged(Editable s) { // Does nothing. Needed to implement TextWatcher. } private Rect mGlobalVisibleRect = new Rect(); private Point mGlobalVisibleOffset = new Point(); public int getActionModeGlobalBottom() { if (mActionMode == null) { return 0; } View view = (View) mCustomView.getParent(); if (view == null) { view = mCustomView; } view.getGlobalVisibleRect(mGlobalVisibleRect, mGlobalVisibleOffset); return mGlobalVisibleRect.bottom; } public static class NoAction implements ActionMode.Callback { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { return false; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { return false; } @Override public void onDestroyActionMode(ActionMode mode) { } } }