• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 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 com.android.inputmethod.latin.utils;
18 
19 import android.text.InputType;
20 import android.text.TextUtils;
21 
22 import com.android.inputmethod.latin.Constants;
23 import com.android.inputmethod.latin.WordComposer;
24 import com.android.inputmethod.latin.settings.SettingsValues;
25 
26 import java.util.Locale;
27 
28 public final class CapsModeUtils {
CapsModeUtils()29     private CapsModeUtils() {
30         // This utility class is not publicly instantiable.
31     }
32 
33     /**
34      * Apply an auto-caps mode to a string.
35      *
36      * This intentionally does NOT apply manual caps mode. It only changes the capitalization if
37      * the mode is one of the auto-caps modes.
38      * @param s The string to capitalize.
39      * @param capitalizeMode The mode in which to capitalize.
40      * @param locale The locale for capitalizing.
41      * @return The capitalized string.
42      */
applyAutoCapsMode(final String s, final int capitalizeMode, final Locale locale)43     public static String applyAutoCapsMode(final String s, final int capitalizeMode,
44             final Locale locale) {
45         if (WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == capitalizeMode) {
46             return s.toUpperCase(locale);
47         } else if (WordComposer.CAPS_MODE_AUTO_SHIFTED == capitalizeMode) {
48             return StringUtils.capitalizeFirstCodePoint(s, locale);
49         } else {
50             return s;
51         }
52     }
53 
54     /**
55      * Return whether a constant represents an auto-caps mode (either auto-shift or auto-shift-lock)
56      * @param mode The mode to test for
57      * @return true if this represents an auto-caps mode, false otherwise
58      */
isAutoCapsMode(final int mode)59     public static boolean isAutoCapsMode(final int mode) {
60         return WordComposer.CAPS_MODE_AUTO_SHIFTED == mode
61                 || WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == mode;
62     }
63 
64     /**
65      * Determine what caps mode should be in effect at the current offset in
66      * the text. Only the mode bits set in <var>reqModes</var> will be
67      * checked. Note that the caps mode flags here are explicitly defined
68      * to match those in {@link InputType}.
69      *
70      * This code is a straight copy of TextUtils.getCapsMode (modulo namespace and formatting
71      * issues). This will change in the future as we simplify the code for our use and fix bugs.
72      *
73      * @param cs The text that should be checked for caps modes.
74      * @param reqModes The modes to be checked: may be any combination of
75      * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and
76      * {@link TextUtils#CAP_MODE_SENTENCES}.
77      * @param settingsValues The current settings values.
78      * @param hasSpaceBefore Whether we should consider there is a space inserted at the end of cs
79      *
80      * @return Returns the actual capitalization modes that can be in effect
81      * at the current position, which is any combination of
82      * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and
83      * {@link TextUtils#CAP_MODE_SENTENCES}.
84      */
getCapsMode(final CharSequence cs, final int reqModes, final SettingsValues settingsValues, final boolean hasSpaceBefore)85     public static int getCapsMode(final CharSequence cs, final int reqModes,
86             final SettingsValues settingsValues, final boolean hasSpaceBefore) {
87         // Quick description of what we want to do:
88         // CAP_MODE_CHARACTERS is always on.
89         // CAP_MODE_WORDS is on if there is some whitespace before the cursor.
90         // CAP_MODE_SENTENCES is on if there is some whitespace before the cursor, and the end
91         //   of a sentence just before that.
92         // We ignore opening parentheses and the like just before the cursor for purposes of
93         // finding whitespace for WORDS and SENTENCES modes.
94         // The end of a sentence ends with a period, question mark or exclamation mark. If it's
95         // a period, it also needs not to be an abbreviation, which means it also needs to either
96         // be immediately preceded by punctuation, or by a string of only letters with single
97         // periods interleaved.
98 
99         // Step 1 : check for cap MODE_CHARACTERS. If it's looked for, it's always on.
100         if ((reqModes & (TextUtils.CAP_MODE_WORDS | TextUtils.CAP_MODE_SENTENCES)) == 0) {
101             // Here we are not looking for MODE_WORDS or MODE_SENTENCES, so since we already
102             // evaluated MODE_CHARACTERS, we can return.
103             return TextUtils.CAP_MODE_CHARACTERS & reqModes;
104         }
105 
106         // Step 2 : Skip (ignore at the end of input) any opening punctuation. This includes
107         // opening parentheses, brackets, opening quotes, everything that *opens* a span of
108         // text in the linguistic sense. In RTL languages, this is still an opening sign, although
109         // it may look like a right parenthesis for example. We also include double quote and
110         // single quote since they aren't start punctuation in the unicode sense, but should still
111         // be skipped for English. TODO: does this depend on the language?
112         int i;
113         if (hasSpaceBefore) {
114             i = cs.length() + 1;
115         } else {
116             for (i = cs.length(); i > 0; i--) {
117                 final char c = cs.charAt(i - 1);
118                 if (c != Constants.CODE_DOUBLE_QUOTE && c != Constants.CODE_SINGLE_QUOTE
119                         && Character.getType(c) != Character.START_PUNCTUATION) {
120                     break;
121                 }
122             }
123         }
124 
125         // We are now on the character that precedes any starting punctuation, so in the most
126         // frequent case this will be whitespace or a letter, although it may occasionally be a
127         // start of line, or some symbol.
128 
129         // Step 3 : Search for the start of a paragraph. From the starting point computed in step 2,
130         // we go back over any space or tab char sitting there. We find the start of a paragraph
131         // if the first char that's not a space or tab is a start of line (as in \n, start of text,
132         // or some other similar characters).
133         int j = i;
134         char prevChar = Constants.CODE_SPACE;
135         if (hasSpaceBefore) --j;
136         while (j > 0) {
137             prevChar = cs.charAt(j - 1);
138             if (!Character.isSpaceChar(prevChar) && prevChar != Constants.CODE_TAB) break;
139             j--;
140         }
141         if (j <= 0 || Character.isWhitespace(prevChar)) {
142             // There are only spacing chars between the start of the paragraph and the cursor,
143             // defined as a isWhitespace() char that is neither a isSpaceChar() nor a tab. Both
144             // MODE_WORDS and MODE_SENTENCES should be active.
145             return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS
146                     | TextUtils.CAP_MODE_SENTENCES) & reqModes;
147         }
148         if (i == j) {
149             // If we don't have whitespace before index i, it means neither MODE_WORDS
150             // nor mode sentences should be on so we can return right away.
151             return TextUtils.CAP_MODE_CHARACTERS & reqModes;
152         }
153         if ((reqModes & TextUtils.CAP_MODE_SENTENCES) == 0) {
154             // Here we know we have whitespace before the cursor (if not, we returned in the above
155             // if i == j clause), so we need MODE_WORDS to be on. And we don't need to evaluate
156             // MODE_SENTENCES so we can return right away.
157             return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
158         }
159         // Please note that because of the reqModes & CAP_MODE_SENTENCES test a few lines above,
160         // we know that MODE_SENTENCES is being requested.
161 
162         // Step 4 : Search for MODE_SENTENCES.
163         // English is a special case in that "American typography" rules, which are the most common
164         // in English, state that a sentence terminator immediately following a quotation mark
165         // should be swapped with it and de-duplicated (included in the quotation mark),
166         // e.g. <<Did he say, "let's go home?">>
167         // No other language has such a rule as far as I know, instead putting inside the quotation
168         // mark as the exact thing quoted and handling the surrounding punctuation independently,
169         // e.g. <<Did he say, "let's go home"?>>
170         // Hence, specifically for English, we treat this special case here.
171         if (Locale.ENGLISH.getLanguage().equals(settingsValues.mLocale.getLanguage())) {
172             for (; j > 0; j--) {
173                 // Here we look to go over any closing punctuation. This is because in dominant
174                 // variants of English, the final period is placed within double quotes and maybe
175                 // other closing punctuation signs. This is generally not true in other languages.
176                 final char c = cs.charAt(j - 1);
177                 if (c != Constants.CODE_DOUBLE_QUOTE && c != Constants.CODE_SINGLE_QUOTE
178                         && Character.getType(c) != Character.END_PUNCTUATION) {
179                     break;
180                 }
181             }
182         }
183 
184         if (j <= 0) return TextUtils.CAP_MODE_CHARACTERS & reqModes;
185         char c = cs.charAt(--j);
186 
187         // We found the next interesting chunk of text ; next we need to determine if it's the
188         // end of a sentence. If we have a question mark or an exclamation mark, it's the end of
189         // a sentence. If it's neither, the only remaining case is the period so we get the opposite
190         // case out of the way.
191         if (c == Constants.CODE_QUESTION_MARK || c == Constants.CODE_EXCLAMATION_MARK) {
192             return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_SENTENCES) & reqModes;
193         }
194         if (settingsValues.mSentenceSeparator != c || j <= 0) {
195             return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
196         }
197 
198         // We found out that we have a period. We need to determine if this is a full stop or
199         // otherwise sentence-ending period, or an abbreviation like "e.g.". An abbreviation
200         // looks like (\w\.){2,}
201         // To find out, we will have a simple state machine with the following states :
202         // START, WORD, PERIOD, ABBREVIATION
203         // On START : (just before the first period)
204         //           letter => WORD
205         //           whitespace => end with no caps (it was a stand-alone period)
206         //           otherwise => end with caps (several periods/symbols in a row)
207         // On WORD : (within the word just before the first period)
208         //           letter => WORD
209         //           period => PERIOD
210         //           otherwise => end with caps (it was a word with a full stop at the end)
211         // On PERIOD : (period within a potential abbreviation)
212         //           letter => LETTER
213         //           otherwise => end with caps (it was not an abbreviation)
214         // On LETTER : (letter within a potential abbreviation)
215         //           letter => LETTER
216         //           period => PERIOD
217         //           otherwise => end with no caps (it was an abbreviation)
218         // "Not an abbreviation" in the above chart essentially covers cases like "...yes.". This
219         // should capitalize.
220 
221         final int START = 0;
222         final int WORD = 1;
223         final int PERIOD = 2;
224         final int LETTER = 3;
225         final int caps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS
226                 | TextUtils.CAP_MODE_SENTENCES) & reqModes;
227         final int noCaps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
228         int state = START;
229         while (j > 0) {
230             c = cs.charAt(--j);
231             switch (state) {
232             case START:
233                 if (Character.isLetter(c)) {
234                     state = WORD;
235                 } else if (Character.isWhitespace(c)) {
236                     return noCaps;
237                 } else {
238                     return caps;
239                 }
240                 break;
241             case WORD:
242                 if (Character.isLetter(c)) {
243                     state = WORD;
244                 } else if (settingsValues.mSentenceSeparator == c) {
245                     state = PERIOD;
246                 } else {
247                     return caps;
248                 }
249                 break;
250             case PERIOD:
251                 if (Character.isLetter(c)) {
252                     state = LETTER;
253                 } else {
254                     return caps;
255                 }
256                 break;
257             case LETTER:
258                 if (Character.isLetter(c)) {
259                     state = LETTER;
260                 } else if (settingsValues.mSentenceSeparator == c) {
261                     state = PERIOD;
262                 } else {
263                     return noCaps;
264                 }
265             }
266         }
267         // Here we arrived at the start of the line. This should behave exactly like whitespace.
268         return (START == state || LETTER == state) ? noCaps : caps;
269     }
270 }
271