BaseKeyListener.java revision 375f3153a93b802cf9edf824f30b230c42bb3e41
1/*
2 * Copyright (C) 2006 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
17package android.text.method;
18
19import android.view.KeyEvent;
20import android.view.View;
21import android.text.*;
22import android.text.method.TextKeyListener.Capitalize;
23import android.widget.TextView;
24
25import java.text.BreakIterator;
26
27/**
28 * Abstract base class for key listeners.
29 *
30 * Provides a basic foundation for entering and editing text.
31 * Subclasses should override {@link #onKeyDown} and {@link #onKeyUp} to insert
32 * characters as keys are pressed.
33 * <p></p>
34 * As for all implementations of {@link KeyListener}, this class is only concerned
35 * with hardware keyboards.  Software input methods have no obligation to trigger
36 * the methods in this class.
37 */
38public abstract class BaseKeyListener extends MetaKeyKeyListener
39        implements KeyListener {
40    /* package */ static final Object OLD_SEL_START = new NoCopySpan.Concrete();
41
42    /**
43     * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_DEL} key in
44     * a {@link TextView}.  If there is a selection, deletes the selection; otherwise,
45     * deletes the character before the cursor, if any; ALT+DEL deletes everything on
46     * the line the cursor is on.
47     *
48     * @return true if anything was deleted; false otherwise.
49     */
50    public boolean backspace(View view, Editable content, int keyCode, KeyEvent event) {
51        return backspaceOrForwardDelete(view, content, keyCode, event, false);
52    }
53
54    /**
55     * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_FORWARD_DEL}
56     * key in a {@link TextView}.  If there is a selection, deletes the selection; otherwise,
57     * deletes the character before the cursor, if any; ALT+FORWARD_DEL deletes everything on
58     * the line the cursor is on.
59     *
60     * @return true if anything was deleted; false otherwise.
61     */
62    public boolean forwardDelete(View view, Editable content, int keyCode, KeyEvent event) {
63        return backspaceOrForwardDelete(view, content, keyCode, event, true);
64    }
65
66    private boolean backspaceOrForwardDelete(View view, Editable content, int keyCode,
67            KeyEvent event, boolean isForwardDelete) {
68        // Ensure the key event does not have modifiers except ALT or SHIFT or CTRL.
69        if (!KeyEvent.metaStateHasNoModifiers(event.getMetaState()
70                & ~(KeyEvent.META_SHIFT_MASK | KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK))) {
71            return false;
72        }
73
74        // If there is a current selection, delete it.
75        if (deleteSelection(view, content)) {
76            return true;
77        }
78
79        // MetaKeyKeyListener doesn't track control key state. Need to check the KeyEvent instead.
80        boolean isCtrlActive = ((event.getMetaState() & KeyEvent.META_CTRL_ON) != 0);
81        boolean isShiftActive = (getMetaState(content, META_SHIFT_ON, event) == 1);
82        boolean isAltActive = (getMetaState(content, META_ALT_ON, event) == 1);
83
84        if (isCtrlActive) {
85            if (isAltActive || isShiftActive) {
86                // Ctrl+Alt, Ctrl+Shift, Ctrl+Alt+Shift should not delete any characters.
87                return false;
88            }
89            return deleteUntilWordBoundary(view, content, isForwardDelete);
90        }
91
92        // Alt+Backspace or Alt+ForwardDelete deletes the current line, if possible.
93        if (isAltActive && deleteLine(view, content)) {
94            return true;
95        }
96
97        // Delete a character.
98        final int start = Selection.getSelectionEnd(content);
99        final int end;
100        if (isForwardDelete) {
101            end = TextUtils.getOffsetAfter(content, start);
102        } else {
103            end = TextUtils.getOffsetBefore(content, start);
104        }
105        if (start != end) {
106            content.delete(Math.min(start, end), Math.max(start, end));
107            return true;
108        }
109        return false;
110    }
111
112    private boolean deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete) {
113        int currentCursorOffset = Selection.getSelectionStart(content);
114
115        // If there is a selection, do nothing.
116        if (currentCursorOffset != Selection.getSelectionEnd(content)) {
117            return false;
118        }
119
120        // Early exit if there is no contents to delete.
121        if ((!isForwardDelete && currentCursorOffset == 0) ||
122            (isForwardDelete && currentCursorOffset == content.length())) {
123            return false;
124        }
125
126        WordIterator wordIterator = null;
127        if (view instanceof TextView) {
128            wordIterator = ((TextView)view).getWordIterator();
129        }
130
131        if (wordIterator == null) {
132            // Default locale is used for WordIterator since the appropriate locale is not clear
133            // here.
134            // TODO: Use appropriate locale for WordIterator.
135            wordIterator = new WordIterator();
136        }
137
138        int deleteFrom;
139        int deleteTo;
140
141        if (isForwardDelete) {
142            deleteFrom = currentCursorOffset;
143            wordIterator.setCharSequence(content, deleteFrom, content.length());
144            deleteTo = wordIterator.following(currentCursorOffset);
145            if (deleteTo == BreakIterator.DONE) {
146                deleteTo = content.length();
147            }
148        } else {
149            deleteTo = currentCursorOffset;
150            wordIterator.setCharSequence(content, 0, deleteTo);
151            deleteFrom = wordIterator.preceding(currentCursorOffset);
152            if (deleteFrom == BreakIterator.DONE) {
153                deleteFrom = 0;
154            }
155        }
156        content.delete(deleteFrom, deleteTo);
157        return true;
158    }
159
160    private boolean deleteSelection(View view, Editable content) {
161        int selectionStart = Selection.getSelectionStart(content);
162        int selectionEnd = Selection.getSelectionEnd(content);
163        if (selectionEnd < selectionStart) {
164            int temp = selectionEnd;
165            selectionEnd = selectionStart;
166            selectionStart = temp;
167        }
168        if (selectionStart != selectionEnd) {
169            content.delete(selectionStart, selectionEnd);
170            return true;
171        }
172        return false;
173    }
174
175    private boolean deleteLine(View view, Editable content) {
176        if (view instanceof TextView) {
177            final Layout layout = ((TextView) view).getLayout();
178            if (layout != null) {
179                final int line = layout.getLineForOffset(Selection.getSelectionStart(content));
180                final int start = layout.getLineStart(line);
181                final int end = layout.getLineEnd(line);
182                if (end != start) {
183                    content.delete(start, end);
184                    return true;
185                }
186            }
187        }
188        return false;
189    }
190
191    static int makeTextContentType(Capitalize caps, boolean autoText) {
192        int contentType = InputType.TYPE_CLASS_TEXT;
193        switch (caps) {
194            case CHARACTERS:
195                contentType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
196                break;
197            case WORDS:
198                contentType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
199                break;
200            case SENTENCES:
201                contentType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
202                break;
203        }
204        if (autoText) {
205            contentType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
206        }
207        return contentType;
208    }
209
210    public boolean onKeyDown(View view, Editable content,
211                             int keyCode, KeyEvent event) {
212        boolean handled;
213        switch (keyCode) {
214            case KeyEvent.KEYCODE_DEL:
215                handled = backspace(view, content, keyCode, event);
216                break;
217            case KeyEvent.KEYCODE_FORWARD_DEL:
218                handled = forwardDelete(view, content, keyCode, event);
219                break;
220            default:
221                handled = false;
222                break;
223        }
224
225        if (handled) {
226            adjustMetaAfterKeypress(content);
227        }
228
229        return super.onKeyDown(view, content, keyCode, event);
230    }
231
232    /**
233     * Base implementation handles ACTION_MULTIPLE KEYCODE_UNKNOWN by inserting
234     * the event's text into the content.
235     */
236    public boolean onKeyOther(View view, Editable content, KeyEvent event) {
237        if (event.getAction() != KeyEvent.ACTION_MULTIPLE
238                || event.getKeyCode() != KeyEvent.KEYCODE_UNKNOWN) {
239            // Not something we are interested in.
240            return false;
241        }
242
243        int selectionStart = Selection.getSelectionStart(content);
244        int selectionEnd = Selection.getSelectionEnd(content);
245        if (selectionEnd < selectionStart) {
246            int temp = selectionEnd;
247            selectionEnd = selectionStart;
248            selectionStart = temp;
249        }
250
251        CharSequence text = event.getCharacters();
252        if (text == null) {
253            return false;
254        }
255
256        content.replace(selectionStart, selectionEnd, text);
257        return true;
258    }
259}
260
261