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