blob: deca910c6fbfb05423a45dd5aa1c9a98207b4c61 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package android.view.inputmethod;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.os.Bundle;
22import android.os.Handler;
23import android.os.SystemClock;
24import android.text.Editable;
25import android.text.NoCopySpan;
26import android.text.Selection;
27import android.text.Spannable;
28import android.text.SpannableStringBuilder;
29import android.text.Spanned;
30import android.text.TextUtils;
31import android.text.method.MetaKeyKeyListener;
32import android.util.Log;
33import android.util.LogPrinter;
34import android.view.KeyCharacterMap;
35import android.view.KeyEvent;
36import android.view.View;
37import android.view.ViewRoot;
38
39class ComposingText implements NoCopySpan {
40}
41
42/**
43 * Base class for implementors of the InputConnection interface, taking care
44 * of most of the common behavior for providing a connection to an Editable.
45 * Implementors of this class will want to be sure to implement
46 * {@link #getEditable} to provide access to their own editable object.
47 */
48public class BaseInputConnection implements InputConnection {
49 private static final boolean DEBUG = false;
50 private static final String TAG = "BaseInputConnection";
51 static final Object COMPOSING = new ComposingText();
52
53 final InputMethodManager mIMM;
54 final Handler mH;
55 final View mTargetView;
56 final boolean mDummyMode;
57
58 private Object[] mDefaultComposingSpans;
59
60 Editable mEditable;
61 KeyCharacterMap mKeyCharacterMap;
62
63 BaseInputConnection(InputMethodManager mgr, boolean dummyMode) {
64 mIMM = mgr;
65 mTargetView = null;
66 mH = null;
67 mDummyMode = dummyMode;
68 }
69
70 public BaseInputConnection(View targetView, boolean dummyMode) {
71 mIMM = (InputMethodManager)targetView.getContext().getSystemService(
72 Context.INPUT_METHOD_SERVICE);
73 mH = targetView.getHandler();
74 mTargetView = targetView;
75 mDummyMode = dummyMode;
76 }
77
78 public static final void removeComposingSpans(Spannable text) {
79 text.removeSpan(COMPOSING);
80 Object[] sps = text.getSpans(0, text.length(), Object.class);
81 if (sps != null) {
82 for (int i=sps.length-1; i>=0; i--) {
83 Object o = sps[i];
84 if ((text.getSpanFlags(o)&Spanned.SPAN_COMPOSING) != 0) {
85 text.removeSpan(o);
86 }
87 }
88 }
89 }
90
91 public static void setComposingSpans(Spannable text) {
92 final Object[] sps = text.getSpans(0, text.length(), Object.class);
93 if (sps != null) {
94 for (int i=sps.length-1; i>=0; i--) {
95 final Object o = sps[i];
96 if (o == COMPOSING) {
97 text.removeSpan(o);
98 continue;
99 }
100 final int fl = text.getSpanFlags(o);
101 if ((fl&(Spanned.SPAN_COMPOSING|Spanned.SPAN_POINT_MARK_MASK))
102 != (Spanned.SPAN_COMPOSING|Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)) {
103 text.setSpan(o, text.getSpanStart(o), text.getSpanEnd(o),
104 (fl&Spanned.SPAN_POINT_MARK_MASK)
105 | Spanned.SPAN_COMPOSING
106 | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
107 }
108 }
109 }
110
111 text.setSpan(COMPOSING, 0, text.length(),
112 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
113 }
114
115 public static int getComposingSpanStart(Spannable text) {
116 return text.getSpanStart(COMPOSING);
117 }
118
119 public static int getComposingSpanEnd(Spannable text) {
120 return text.getSpanEnd(COMPOSING);
121 }
122
123 /**
124 * Return the target of edit operations. The default implementation
125 * returns its own fake editable that is just used for composing text;
126 * subclasses that are real text editors should override this and
127 * supply their own.
128 */
129 public Editable getEditable() {
130 if (mEditable == null) {
131 mEditable = Editable.Factory.getInstance().newEditable("");
132 Selection.setSelection(mEditable, 0);
133 }
134 return mEditable;
135 }
136
137 /**
138 * Default implementation does nothing.
139 */
140 public boolean beginBatchEdit() {
141 return false;
142 }
143
144 /**
145 * Default implementation does nothing.
146 */
147 public boolean endBatchEdit() {
148 return false;
149 }
150
151 /**
152 * Default implementation uses
153 * {@link MetaKeyKeyListener#clearMetaKeyState(long, int)
154 * MetaKeyKeyListener.clearMetaKeyState(long, int)} to clear the state.
155 */
156 public boolean clearMetaKeyStates(int states) {
157 final Editable content = getEditable();
158 if (content == null) return false;
159 MetaKeyKeyListener.clearMetaKeyState(content, states);
160 return true;
161 }
162
163 /**
164 * Default implementation does nothing.
165 */
166 public boolean commitCompletion(CompletionInfo text) {
167 return false;
168 }
169
170 /**
171 * Default implementation replaces any existing composing text with
172 * the given text. In addition, only if dummy mode, a key event is
173 * sent for the new text and the current editable buffer cleared.
174 */
175 public boolean commitText(CharSequence text, int newCursorPosition) {
176 if (DEBUG) Log.v(TAG, "commitText " + text);
177 replaceText(text, newCursorPosition, false);
178 sendCurrentText();
179 return true;
180 }
181
182 /**
183 * The default implementation performs the deletion around the current
184 * selection position of the editable text.
185 */
186 public boolean deleteSurroundingText(int leftLength, int rightLength) {
187 if (DEBUG) Log.v(TAG, "deleteSurroundingText " + leftLength
188 + " / " + rightLength);
189 final Editable content = getEditable();
190 if (content == null) return false;
191
192 beginBatchEdit();
193
194 int a = Selection.getSelectionStart(content);
195 int b = Selection.getSelectionEnd(content);
196
197 if (a > b) {
198 int tmp = a;
199 a = b;
200 b = tmp;
201 }
202
203 // ignore the composing text.
204 int ca = getComposingSpanStart(content);
205 int cb = getComposingSpanEnd(content);
206 if (cb < ca) {
207 int tmp = ca;
208 ca = cb;
209 cb = tmp;
210 }
211 if (ca != -1 && cb != -1) {
212 if (ca < a) a = ca;
213 if (cb > b) b = cb;
214 }
215
216 int deleted = 0;
217
218 if (leftLength > 0) {
219 int start = a - leftLength;
220 if (start < 0) start = 0;
221 content.delete(start, a);
222 deleted = a - start;
223 }
224
225 if (rightLength > 0) {
226 b = b - deleted;
227
228 int end = b + rightLength;
229 if (end > content.length()) end = content.length();
230
231 content.delete(b, end);
232 }
233
234 endBatchEdit();
235
236 return true;
237 }
238
239 /**
240 * The default implementation removes the composing state from the
241 * current editable text. In addition, only if dummy mode, a key event is
242 * sent for the new text and the current editable buffer cleared.
243 */
244 public boolean finishComposingText() {
245 if (DEBUG) Log.v(TAG, "finishComposingText");
246 final Editable content = getEditable();
247 if (content != null) {
248 beginBatchEdit();
249 removeComposingSpans(content);
250 endBatchEdit();
251 sendCurrentText();
252 }
253 return true;
254 }
255
256 /**
257 * The default implementation uses TextUtils.getCapsMode to get the
258 * cursor caps mode for the current selection position in the editable
259 * text, unless in dummy mode in which case 0 is always returned.
260 */
261 public int getCursorCapsMode(int reqModes) {
262 if (mDummyMode) return 0;
263
264 final Editable content = getEditable();
265 if (content == null) return 0;
266
267 int a = Selection.getSelectionStart(content);
268 int b = Selection.getSelectionEnd(content);
269
270 if (a > b) {
271 int tmp = a;
272 a = b;
273 b = tmp;
274 }
275
276 return TextUtils.getCapsMode(content, a, reqModes);
277 }
278
279 /**
280 * The default implementation always returns null.
281 */
282 public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
283 return null;
284 }
285
286 /**
287 * The default implementation returns the given amount of text from the
288 * current cursor position in the buffer.
289 */
290 public CharSequence getTextBeforeCursor(int length, int flags) {
291 final Editable content = getEditable();
292 if (content == null) return null;
293
294 int a = Selection.getSelectionStart(content);
295 int b = Selection.getSelectionEnd(content);
296
297 if (a > b) {
298 int tmp = a;
299 a = b;
300 b = tmp;
301 }
302
303 if (length > a) {
304 length = a;
305 }
306
307 if ((flags&GET_TEXT_WITH_STYLES) != 0) {
308 return content.subSequence(a - length, a);
309 }
310 return TextUtils.substring(content, a - length, a);
311 }
312
313 /**
314 * The default implementation returns the given amount of text from the
315 * current cursor position in the buffer.
316 */
317 public CharSequence getTextAfterCursor(int length, int flags) {
318 final Editable content = getEditable();
319 if (content == null) return null;
320
321 int a = Selection.getSelectionStart(content);
322 int b = Selection.getSelectionEnd(content);
323
324 if (a > b) {
325 int tmp = a;
326 a = b;
327 b = tmp;
328 }
329
330 if (b + length > content.length()) {
331 length = content.length() - b;
332 }
333
334
335 if ((flags&GET_TEXT_WITH_STYLES) != 0) {
336 return content.subSequence(b, b + length);
337 }
338 return TextUtils.substring(content, b, b + length);
339 }
340
341 /**
342 * The default implementation does nothing.
343 */
344 public boolean performEditorAction(int actionCode) {
345 return false;
346 }
347
348 /**
349 * The default implementation does nothing.
350 */
351 public boolean performContextMenuAction(int id) {
352 return false;
353 }
354
355 /**
356 * The default implementation does nothing.
357 */
358 public boolean performPrivateCommand(String action, Bundle data) {
359 return false;
360 }
361
362 /**
363 * The default implementation places the given text into the editable,
364 * replacing any existing composing text. The new text is marked as
365 * in a composing state with the composing style.
366 */
367 public boolean setComposingText(CharSequence text, int newCursorPosition) {
368 if (DEBUG) Log.v(TAG, "setComposingText " + text);
369 replaceText(text, newCursorPosition, true);
370 return true;
371 }
372
373 /**
374 * The default implementation changes the selection position in the
375 * current editable text.
376 */
377 public boolean setSelection(int start, int end) {
378 if (DEBUG) Log.v(TAG, "setSelection " + start + ", " + end);
379 final Editable content = getEditable();
380 if (content == null) return false;
381 int len = content.length();
382 if (start > len || end > len) {
383 // If the given selection is out of bounds, just ignore it.
384 // Most likely the text was changed out from under the IME,
385 // the the IME is going to have to update all of its state
386 // anyway.
387 return true;
388 }
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700389 if (start == end && MetaKeyKeyListener.getMetaState(content,
390 MetaKeyKeyListener.META_SELECTING) != 0) {
391 // If we are in selection mode, then we want to extend the
392 // selection instead of replacing it.
393 Selection.extendSelection(content, start);
394 } else {
395 Selection.setSelection(content, start, end);
396 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800397 return true;
398 }
399
400 /**
401 * Provides standard implementation for sending a key event to the window
402 * attached to the input connection's view.
403 */
404 public boolean sendKeyEvent(KeyEvent event) {
405 synchronized (mIMM.mH) {
406 Handler h = mH;
407 if (h == null) {
408 if (mIMM.mServedView != null) {
409 h = mIMM.mServedView.getHandler();
410 }
411 }
412 if (h != null) {
413 h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME,
414 event));
415 }
416 }
417 return false;
418 }
419
420 /**
421 * Updates InputMethodManager with the current fullscreen mode.
422 */
423 public boolean reportFullscreenMode(boolean enabled) {
424 mIMM.setFullscreenMode(enabled);
425 return true;
426 }
427
428 private void sendCurrentText() {
429 if (!mDummyMode) {
430 return;
431 }
432
433 Editable content = getEditable();
434 if (content != null) {
435 final int N = content.length();
436 if (N == 0) {
437 return;
438 }
439 if (N == 1) {
440 // If it's 1 character, we have a chance of being
441 // able to generate normal key events...
442 if (mKeyCharacterMap == null) {
443 mKeyCharacterMap = KeyCharacterMap.load(
444 KeyCharacterMap.BUILT_IN_KEYBOARD);
445 }
446 char[] chars = new char[1];
447 content.getChars(0, 1, chars, 0);
448 KeyEvent[] events = mKeyCharacterMap.getEvents(chars);
449 if (events != null) {
450 for (int i=0; i<events.length; i++) {
451 if (DEBUG) Log.v(TAG, "Sending: " + events[i]);
452 sendKeyEvent(events[i]);
453 }
454 content.clear();
455 return;
456 }
457 }
458
459 // Otherwise, revert to the special key event containing
460 // the actual characters.
461 KeyEvent event = new KeyEvent(SystemClock.uptimeMillis(),
462 content.toString(), KeyCharacterMap.BUILT_IN_KEYBOARD, 0);
463 sendKeyEvent(event);
464 content.clear();
465 }
466 }
467
468 private void replaceText(CharSequence text, int newCursorPosition,
469 boolean composing) {
470 final Editable content = getEditable();
471 if (content == null) {
472 return;
473 }
474
475 beginBatchEdit();
476
477 // delete composing text set previously.
478 int a = getComposingSpanStart(content);
479 int b = getComposingSpanEnd(content);
480
481 if (DEBUG) Log.v(TAG, "Composing span: " + a + " to " + b);
482
483 if (b < a) {
484 int tmp = a;
485 a = b;
486 b = tmp;
487 }
488
489 if (a != -1 && b != -1) {
490 removeComposingSpans(content);
491 } else {
492 a = Selection.getSelectionStart(content);
493 b = Selection.getSelectionEnd(content);
494 if (a >=0 && b>= 0 && a != b) {
495 if (b < a) {
496 int tmp = a;
497 a = b;
498 b = tmp;
499 }
500 }
501 }
502
503 if (composing) {
504 Spannable sp = null;
505 if (!(text instanceof Spannable)) {
506 sp = new SpannableStringBuilder(text);
507 text = sp;
508 if (mDefaultComposingSpans == null) {
509 Context context;
510 if (mTargetView != null) {
511 context = mTargetView.getContext();
512 } else if (mIMM.mServedView != null) {
513 context = mIMM.mServedView.getContext();
514 } else {
515 context = null;
516 }
517 if (context != null) {
518 TypedArray ta = context.getTheme()
519 .obtainStyledAttributes(new int[] {
520 com.android.internal.R.attr.candidatesTextStyleSpans
521 });
522 CharSequence style = ta.getText(0);
523 ta.recycle();
524 if (style != null && style instanceof Spanned) {
525 mDefaultComposingSpans = ((Spanned)style).getSpans(
526 0, style.length(), Object.class);
527 }
528 }
529 }
530 if (mDefaultComposingSpans != null) {
531 for (int i = 0; i < mDefaultComposingSpans.length; ++i) {
532 sp.setSpan(mDefaultComposingSpans[i], 0, sp.length(),
533 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
534 }
535 }
536 } else {
537 sp = (Spannable)text;
538 }
539 setComposingSpans(sp);
540 }
541
542 if (DEBUG) Log.v(TAG, "Replacing from " + a + " to " + b + " with \""
543 + text + "\", composing=" + composing
544 + ", type=" + text.getClass().getCanonicalName());
545
546 if (DEBUG) {
547 LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG);
548 lp.println("Current text:");
549 TextUtils.dumpSpans(content, lp, " ");
550 lp.println("Composing text:");
551 TextUtils.dumpSpans(text, lp, " ");
552 }
553
554 // Position the cursor appropriately, so that after replacing the
555 // desired range of text it will be located in the correct spot.
556 // This allows us to deal with filters performing edits on the text
557 // we are providing here.
558 if (newCursorPosition > 0) {
559 newCursorPosition += b - 1;
560 } else {
561 newCursorPosition += a;
562 }
563 if (newCursorPosition < 0) newCursorPosition = 0;
564 if (newCursorPosition > content.length())
565 newCursorPosition = content.length();
566 Selection.setSelection(content, newCursorPosition);
567
568 content.replace(a, b, text);
569
570 if (DEBUG) {
571 LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG);
572 lp.println("Final text:");
573 TextUtils.dumpSpans(content, lp, " ");
574 }
575
576 endBatchEdit();
577 }
578}