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