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 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 = (InputMethodManager) 65 context.getSystemService(Context.INPUT_METHOD_SERVICE); 66 mResources = context.getResources(); 67 } 68 69 public void finish() { 70 mActionMode.finish(); 71 } 72 73 /* 74 * Place text in the text field so it can be searched for. Need to press 75 * the find next or find previous button to find all of the matches. 76 */ 77 public void setText(String text) { 78 mEditText.setText(text); 79 Spannable span = (Spannable) mEditText.getText(); 80 int length = span.length(); 81 // Ideally, we would like to set the selection to the whole field, 82 // but this brings up the Text selection CAB, which dismisses this 83 // one. 84 Selection.setSelection(span, length, length); 85 // Necessary each time we set the text, so that this will watch 86 // changes to it. 87 span.setSpan(this, 0, length, Spannable.SPAN_INCLUSIVE_INCLUSIVE); 88 mMatchesFound = false; 89 } 90 91 /* 92 * Set the WebView to search. Must be non null. 93 */ 94 public void setWebView(WebView webView) { 95 if (null == webView) { 96 throw new AssertionError("WebView supplied to " 97 + "FindActionModeCallback cannot be null"); 98 } 99 mWebView = webView; 100 mWebView.setFindDialogFindListener(this); 101 } 102 103 @Override 104 public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, 105 boolean isDoneCounting) { 106 if (isDoneCounting) { 107 updateMatchCount(activeMatchOrdinal, numberOfMatches, numberOfMatches == 0); 108 } 109 } 110 111 /* 112 * Move the highlight to the next match. 113 * @param next If true, find the next match further down in the document. 114 * If false, find the previous match, up in the document. 115 */ 116 private void findNext(boolean next) { 117 if (mWebView == null) { 118 throw new AssertionError( 119 "No WebView for FindActionModeCallback::findNext"); 120 } 121 if (!mMatchesFound) { 122 findAll(); 123 return; 124 } 125 if (0 == mNumberOfMatches) { 126 // There are no matches, so moving to the next match will not do 127 // anything. 128 return; 129 } 130 mWebView.findNext(next); 131 updateMatchesString(); 132 } 133 134 /* 135 * Highlight all the instances of the string from mEditText in mWebView. 136 */ 137 public void findAll() { 138 if (mWebView == null) { 139 throw new AssertionError( 140 "No WebView for FindActionModeCallback::findAll"); 141 } 142 CharSequence find = mEditText.getText(); 143 if (0 == find.length()) { 144 mWebView.clearMatches(); 145 mMatches.setVisibility(View.GONE); 146 mMatchesFound = false; 147 mWebView.findAll(null); 148 } else { 149 mMatchesFound = true; 150 mMatches.setVisibility(View.INVISIBLE); 151 mNumberOfMatches = 0; 152 mWebView.findAllAsync(find.toString()); 153 } 154 } 155 156 public void showSoftInput() { 157 if (mEditText.requestFocus()) { 158 mInput.showSoftInput(mEditText, 0); 159 } 160 } 161 162 public void updateMatchCount(int matchIndex, int matchCount, boolean isEmptyFind) { 163 if (!isEmptyFind) { 164 mNumberOfMatches = matchCount; 165 mActiveMatchIndex = matchIndex; 166 updateMatchesString(); 167 } else { 168 mMatches.setVisibility(View.GONE); 169 mNumberOfMatches = 0; 170 } 171 } 172 173 /* 174 * Update the string which tells the user how many matches were found, and 175 * which match is currently highlighted. 176 */ 177 private void updateMatchesString() { 178 if (mNumberOfMatches == 0) { 179 mMatches.setText(com.android.internal.R.string.no_matches); 180 } else { 181 mMatches.setText(mResources.getQuantityString( 182 com.android.internal.R.plurals.matches_found, mNumberOfMatches, 183 mActiveMatchIndex + 1, mNumberOfMatches)); 184 } 185 mMatches.setVisibility(View.VISIBLE); 186 } 187 188 // OnClickListener implementation 189 190 @Override 191 public void onClick(View v) { 192 findNext(true); 193 } 194 195 // ActionMode.Callback implementation 196 197 @Override 198 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 199 if (!mode.isUiFocusable()) { 200 // If the action mode we're running in is not focusable the user 201 // will not be able to type into the find on page field. This 202 // should only come up when we're running in a dialog which is 203 // already less than ideal; disable the option for now. 204 return false; 205 } 206 207 mode.setCustomView(mCustomView); 208 mode.getMenuInflater().inflate(com.android.internal.R.menu.webview_find, 209 menu); 210 mActionMode = mode; 211 Editable edit = mEditText.getText(); 212 Selection.setSelection(edit, edit.length()); 213 mMatches.setVisibility(View.GONE); 214 mMatchesFound = false; 215 mMatches.setText("0"); 216 mEditText.requestFocus(); 217 return true; 218 } 219 220 @Override 221 public void onDestroyActionMode(ActionMode mode) { 222 mActionMode = null; 223 mWebView.notifyFindDialogDismissed(); 224 mWebView.setFindDialogFindListener(null); 225 mInput.hideSoftInputFromWindow(mWebView.getWindowToken(), 0); 226 } 227 228 @Override 229 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 230 return false; 231 } 232 233 @Override 234 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 235 if (mWebView == null) { 236 throw new AssertionError( 237 "No WebView for FindActionModeCallback::onActionItemClicked"); 238 } 239 mInput.hideSoftInputFromWindow(mWebView.getWindowToken(), 0); 240 switch(item.getItemId()) { 241 case com.android.internal.R.id.find_prev: 242 findNext(false); 243 break; 244 case com.android.internal.R.id.find_next: 245 findNext(true); 246 break; 247 default: 248 return false; 249 } 250 return true; 251 } 252 253 // TextWatcher implementation 254 255 @Override 256 public void beforeTextChanged(CharSequence s, 257 int start, 258 int count, 259 int after) { 260 // Does nothing. Needed to implement TextWatcher. 261 } 262 263 @Override 264 public void onTextChanged(CharSequence s, 265 int start, 266 int before, 267 int count) { 268 findAll(); 269 } 270 271 @Override 272 public void afterTextChanged(Editable s) { 273 // Does nothing. Needed to implement TextWatcher. 274 } 275 276 private Rect mGlobalVisibleRect = new Rect(); 277 private Point mGlobalVisibleOffset = new Point(); 278 public int getActionModeGlobalBottom() { 279 if (mActionMode == null) { 280 return 0; 281 } 282 View view = (View) mCustomView.getParent(); 283 if (view == null) { 284 view = mCustomView; 285 } 286 view.getGlobalVisibleRect(mGlobalVisibleRect, mGlobalVisibleOffset); 287 return mGlobalVisibleRect.bottom; 288 } 289 290 public static class NoAction implements ActionMode.Callback { 291 @Override 292 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 293 return false; 294 } 295 296 @Override 297 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 298 return false; 299 } 300 301 @Override 302 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 303 return false; 304 } 305 306 @Override 307 public void onDestroyActionMode(ActionMode mode) { 308 } 309 } 310 } 311