1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.webkit; 18 19 import android.annotation.SystemApi; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.graphics.Point; 23 import android.graphics.Rect; 24 import android.text.Editable; 25 import android.text.Selection; 26 import android.text.Spannable; 27 import android.text.TextWatcher; 28 import android.view.ActionMode; 29 import android.view.LayoutInflater; 30 import android.view.Menu; 31 import android.view.MenuItem; 32 import android.view.View; 33 import android.view.inputmethod.InputMethodManager; 34 import android.widget.EditText; 35 import android.widget.TextView; 36 37 /** 38 * @hide 39 */ 40 @SystemApi 41 public class FindActionModeCallback implements ActionMode.Callback, TextWatcher, 42 View.OnClickListener, WebView.FindListener { 43 private View mCustomView; 44 private EditText mEditText; 45 private TextView mMatches; 46 private WebView mWebView; 47 private InputMethodManager mInput; 48 private Resources mResources; 49 private boolean mMatchesFound; 50 private int mNumberOfMatches; 51 private int mActiveMatchIndex; 52 private ActionMode mActionMode; 53 FindActionModeCallback(Context context)54 public FindActionModeCallback(Context context) { 55 mCustomView = LayoutInflater.from(context).inflate( 56 com.android.internal.R.layout.webview_find, null); 57 mEditText = (EditText) mCustomView.findViewById( 58 com.android.internal.R.id.edit); 59 mEditText.setCustomSelectionActionModeCallback(new NoAction()); 60 mEditText.setOnClickListener(this); 61 setText(""); 62 mMatches = (TextView) mCustomView.findViewById( 63 com.android.internal.R.id.matches); 64 mInput = context.getSystemService(InputMethodManager.class); 65 mResources = context.getResources(); 66 } 67 finish()68 public void finish() { 69 mActionMode.finish(); 70 } 71 72 /* 73 * Place text in the text field so it can be searched for. Need to press 74 * the find next or find previous button to find all of the matches. 75 */ setText(String text)76 public void setText(String text) { 77 mEditText.setText(text); 78 Spannable span = (Spannable) mEditText.getText(); 79 int length = span.length(); 80 // Ideally, we would like to set the selection to the whole field, 81 // but this brings up the Text selection CAB, which dismisses this 82 // one. 83 Selection.setSelection(span, length, length); 84 // Necessary each time we set the text, so that this will watch 85 // changes to it. 86 span.setSpan(this, 0, length, Spannable.SPAN_INCLUSIVE_INCLUSIVE); 87 mMatchesFound = false; 88 } 89 90 /* 91 * Set the WebView to search. Must be non null. 92 */ setWebView(WebView webView)93 public void setWebView(WebView webView) { 94 if (null == webView) { 95 throw new AssertionError("WebView supplied to " 96 + "FindActionModeCallback cannot be null"); 97 } 98 mWebView = webView; 99 mWebView.setFindDialogFindListener(this); 100 } 101 102 @Override onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting)103 public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, 104 boolean isDoneCounting) { 105 if (isDoneCounting) { 106 updateMatchCount(activeMatchOrdinal, numberOfMatches, numberOfMatches == 0); 107 } 108 } 109 110 /* 111 * Move the highlight to the next match. 112 * @param next If true, find the next match further down in the document. 113 * If false, find the previous match, up in the document. 114 */ findNext(boolean next)115 private void findNext(boolean next) { 116 if (mWebView == null) { 117 throw new AssertionError( 118 "No WebView for FindActionModeCallback::findNext"); 119 } 120 if (!mMatchesFound) { 121 findAll(); 122 return; 123 } 124 if (0 == mNumberOfMatches) { 125 // There are no matches, so moving to the next match will not do 126 // anything. 127 return; 128 } 129 mWebView.findNext(next); 130 updateMatchesString(); 131 } 132 133 /* 134 * Highlight all the instances of the string from mEditText in mWebView. 135 */ findAll()136 public void findAll() { 137 if (mWebView == null) { 138 throw new AssertionError( 139 "No WebView for FindActionModeCallback::findAll"); 140 } 141 CharSequence find = mEditText.getText(); 142 if (0 == find.length()) { 143 mWebView.clearMatches(); 144 mMatches.setVisibility(View.GONE); 145 mMatchesFound = false; 146 mWebView.findAll(null); 147 } else { 148 mMatchesFound = true; 149 mMatches.setVisibility(View.INVISIBLE); 150 mNumberOfMatches = 0; 151 mWebView.findAllAsync(find.toString()); 152 } 153 } 154 showSoftInput()155 public void showSoftInput() { 156 if (mEditText.requestFocus()) { 157 mInput.showSoftInput(mEditText, 0); 158 } 159 } 160 updateMatchCount(int matchIndex, int matchCount, boolean isEmptyFind)161 public void updateMatchCount(int matchIndex, int matchCount, boolean isEmptyFind) { 162 if (!isEmptyFind) { 163 mNumberOfMatches = matchCount; 164 mActiveMatchIndex = matchIndex; 165 updateMatchesString(); 166 } else { 167 mMatches.setVisibility(View.GONE); 168 mNumberOfMatches = 0; 169 } 170 } 171 172 /* 173 * Update the string which tells the user how many matches were found, and 174 * which match is currently highlighted. 175 */ updateMatchesString()176 private void updateMatchesString() { 177 if (mNumberOfMatches == 0) { 178 mMatches.setText(com.android.internal.R.string.no_matches); 179 } else { 180 mMatches.setText(mResources.getQuantityString( 181 com.android.internal.R.plurals.matches_found, mNumberOfMatches, 182 mActiveMatchIndex + 1, mNumberOfMatches)); 183 } 184 mMatches.setVisibility(View.VISIBLE); 185 } 186 187 // OnClickListener implementation 188 189 @Override onClick(View v)190 public void onClick(View v) { 191 findNext(true); 192 } 193 194 // ActionMode.Callback implementation 195 196 @Override onCreateActionMode(ActionMode mode, Menu menu)197 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 198 if (!mode.isUiFocusable()) { 199 // If the action mode we're running in is not focusable the user 200 // will not be able to type into the find on page field. This 201 // should only come up when we're running in a dialog which is 202 // already less than ideal; disable the option for now. 203 return false; 204 } 205 206 mode.setCustomView(mCustomView); 207 mode.getMenuInflater().inflate(com.android.internal.R.menu.webview_find, 208 menu); 209 mActionMode = mode; 210 Editable edit = mEditText.getText(); 211 Selection.setSelection(edit, edit.length()); 212 mMatches.setVisibility(View.GONE); 213 mMatchesFound = false; 214 mMatches.setText("0"); 215 mEditText.requestFocus(); 216 return true; 217 } 218 219 @Override onDestroyActionMode(ActionMode mode)220 public void onDestroyActionMode(ActionMode mode) { 221 mActionMode = null; 222 mWebView.notifyFindDialogDismissed(); 223 mWebView.setFindDialogFindListener(null); 224 mInput.hideSoftInputFromWindow(mWebView.getWindowToken(), 0); 225 } 226 227 @Override onPrepareActionMode(ActionMode mode, Menu menu)228 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 229 return false; 230 } 231 232 @Override onActionItemClicked(ActionMode mode, MenuItem item)233 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 234 if (mWebView == null) { 235 throw new AssertionError( 236 "No WebView for FindActionModeCallback::onActionItemClicked"); 237 } 238 mInput.hideSoftInputFromWindow(mWebView.getWindowToken(), 0); 239 switch(item.getItemId()) { 240 case com.android.internal.R.id.find_prev: 241 findNext(false); 242 break; 243 case com.android.internal.R.id.find_next: 244 findNext(true); 245 break; 246 default: 247 return false; 248 } 249 return true; 250 } 251 252 // TextWatcher implementation 253 254 @Override beforeTextChanged(CharSequence s, int start, int count, int after)255 public void beforeTextChanged(CharSequence s, 256 int start, 257 int count, 258 int after) { 259 // Does nothing. Needed to implement TextWatcher. 260 } 261 262 @Override onTextChanged(CharSequence s, int start, int before, int count)263 public void onTextChanged(CharSequence s, 264 int start, 265 int before, 266 int count) { 267 findAll(); 268 } 269 270 @Override afterTextChanged(Editable s)271 public void afterTextChanged(Editable s) { 272 // Does nothing. Needed to implement TextWatcher. 273 } 274 275 private Rect mGlobalVisibleRect = new Rect(); 276 private Point mGlobalVisibleOffset = new Point(); getActionModeGlobalBottom()277 public int getActionModeGlobalBottom() { 278 if (mActionMode == null) { 279 return 0; 280 } 281 View view = (View) mCustomView.getParent(); 282 if (view == null) { 283 view = mCustomView; 284 } 285 view.getGlobalVisibleRect(mGlobalVisibleRect, mGlobalVisibleOffset); 286 return mGlobalVisibleRect.bottom; 287 } 288 289 public static class NoAction implements ActionMode.Callback { 290 @Override onCreateActionMode(ActionMode mode, Menu menu)291 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 292 return false; 293 } 294 295 @Override onPrepareActionMode(ActionMode mode, Menu menu)296 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 297 return false; 298 } 299 300 @Override onActionItemClicked(ActionMode mode, MenuItem item)301 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 302 return false; 303 } 304 305 @Override onDestroyActionMode(ActionMode mode)306 public void onDestroyActionMode(ActionMode mode) { 307 } 308 } 309 } 310