1/*
2 * Copyright (C) 2013 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 com.android.inputmethod.latin.utils;
18
19import com.android.inputmethod.latin.common.StringUtils;
20
21import java.util.Locale;
22
23/**
24 * The status of the current recapitalize process.
25 */
26public class RecapitalizeStatus {
27    public static final int NOT_A_RECAPITALIZE_MODE = -1;
28    public static final int CAPS_MODE_ORIGINAL_MIXED_CASE = 0;
29    public static final int CAPS_MODE_ALL_LOWER = 1;
30    public static final int CAPS_MODE_FIRST_WORD_UPPER = 2;
31    public static final int CAPS_MODE_ALL_UPPER = 3;
32    // When adding a new mode, don't forget to update the CAPS_MODE_LAST constant.
33    public static final int CAPS_MODE_LAST = CAPS_MODE_ALL_UPPER;
34
35    private static final int[] ROTATION_STYLE = {
36        CAPS_MODE_ORIGINAL_MIXED_CASE,
37        CAPS_MODE_ALL_LOWER,
38        CAPS_MODE_FIRST_WORD_UPPER,
39        CAPS_MODE_ALL_UPPER
40    };
41
42    private static final int getStringMode(final String string, final int[] sortedSeparators) {
43        if (StringUtils.isIdenticalAfterUpcase(string)) {
44            return CAPS_MODE_ALL_UPPER;
45        } else if (StringUtils.isIdenticalAfterDowncase(string)) {
46            return CAPS_MODE_ALL_LOWER;
47        } else if (StringUtils.isIdenticalAfterCapitalizeEachWord(string, sortedSeparators)) {
48            return CAPS_MODE_FIRST_WORD_UPPER;
49        } else {
50            return CAPS_MODE_ORIGINAL_MIXED_CASE;
51        }
52    }
53
54    public static String modeToString(final int recapitalizeMode) {
55        switch (recapitalizeMode) {
56        case NOT_A_RECAPITALIZE_MODE: return "undefined";
57        case CAPS_MODE_ORIGINAL_MIXED_CASE: return "mixedCase";
58        case CAPS_MODE_ALL_LOWER: return "allLower";
59        case CAPS_MODE_FIRST_WORD_UPPER: return "firstWordUpper";
60        case CAPS_MODE_ALL_UPPER: return "allUpper";
61        default: return "unknown<" + recapitalizeMode + ">";
62        }
63    }
64
65    /**
66     * We store the location of the cursor and the string that was there before the recapitalize
67     * action was done, and the location of the cursor and the string that was there after.
68     */
69    private int mCursorStartBefore;
70    private String mStringBefore;
71    private int mCursorStartAfter;
72    private int mCursorEndAfter;
73    private int mRotationStyleCurrentIndex;
74    private boolean mSkipOriginalMixedCaseMode;
75    private Locale mLocale;
76    private int[] mSortedSeparators;
77    private String mStringAfter;
78    private boolean mIsStarted;
79    private boolean mIsEnabled = true;
80
81    private static final int[] EMPTY_STORTED_SEPARATORS = {};
82
83    public RecapitalizeStatus() {
84        // By default, initialize with dummy values that won't match any real recapitalize.
85        start(-1, -1, "", Locale.getDefault(), EMPTY_STORTED_SEPARATORS);
86        stop();
87    }
88
89    public void start(final int cursorStart, final int cursorEnd, final String string,
90            final Locale locale, final int[] sortedSeparators) {
91        if (!mIsEnabled) {
92            return;
93        }
94        mCursorStartBefore = cursorStart;
95        mStringBefore = string;
96        mCursorStartAfter = cursorStart;
97        mCursorEndAfter = cursorEnd;
98        mStringAfter = string;
99        final int initialMode = getStringMode(mStringBefore, sortedSeparators);
100        mLocale = locale;
101        mSortedSeparators = sortedSeparators;
102        if (CAPS_MODE_ORIGINAL_MIXED_CASE == initialMode) {
103            mRotationStyleCurrentIndex = 0;
104            mSkipOriginalMixedCaseMode = false;
105        } else {
106            // Find the current mode in the array.
107            int currentMode;
108            for (currentMode = ROTATION_STYLE.length - 1; currentMode > 0; --currentMode) {
109                if (ROTATION_STYLE[currentMode] == initialMode) {
110                    break;
111                }
112            }
113            mRotationStyleCurrentIndex = currentMode;
114            mSkipOriginalMixedCaseMode = true;
115        }
116        mIsStarted = true;
117    }
118
119    public void stop() {
120        mIsStarted = false;
121    }
122
123    public boolean isStarted() {
124        return mIsStarted;
125    }
126
127    public void enable() {
128        mIsEnabled = true;
129    }
130
131    public void disable() {
132        mIsEnabled = false;
133    }
134
135    public boolean mIsEnabled() {
136        return mIsEnabled;
137    }
138
139    public boolean isSetAt(final int cursorStart, final int cursorEnd) {
140        return cursorStart == mCursorStartAfter && cursorEnd == mCursorEndAfter;
141    }
142
143    /**
144     * Rotate through the different possible capitalization modes.
145     */
146    public void rotate() {
147        final String oldResult = mStringAfter;
148        int count = 0; // Protection against infinite loop.
149        do {
150            mRotationStyleCurrentIndex = (mRotationStyleCurrentIndex + 1) % ROTATION_STYLE.length;
151            if (CAPS_MODE_ORIGINAL_MIXED_CASE == ROTATION_STYLE[mRotationStyleCurrentIndex]
152                    && mSkipOriginalMixedCaseMode) {
153                mRotationStyleCurrentIndex =
154                        (mRotationStyleCurrentIndex + 1) % ROTATION_STYLE.length;
155            }
156            ++count;
157            switch (ROTATION_STYLE[mRotationStyleCurrentIndex]) {
158            case CAPS_MODE_ORIGINAL_MIXED_CASE:
159                mStringAfter = mStringBefore;
160                break;
161            case CAPS_MODE_ALL_LOWER:
162                mStringAfter = mStringBefore.toLowerCase(mLocale);
163                break;
164            case CAPS_MODE_FIRST_WORD_UPPER:
165                mStringAfter = StringUtils.capitalizeEachWord(mStringBefore, mSortedSeparators,
166                        mLocale);
167                break;
168            case CAPS_MODE_ALL_UPPER:
169                mStringAfter = mStringBefore.toUpperCase(mLocale);
170                break;
171            default:
172                mStringAfter = mStringBefore;
173            }
174        } while (mStringAfter.equals(oldResult) && count < ROTATION_STYLE.length + 1);
175        mCursorEndAfter = mCursorStartAfter + mStringAfter.length();
176    }
177
178    /**
179     * Remove leading/trailing whitespace from the considered string.
180     */
181    public void trim() {
182        final int len = mStringBefore.length();
183        int nonWhitespaceStart = 0;
184        for (; nonWhitespaceStart < len;
185                nonWhitespaceStart = mStringBefore.offsetByCodePoints(nonWhitespaceStart, 1)) {
186            final int codePoint = mStringBefore.codePointAt(nonWhitespaceStart);
187            if (!Character.isWhitespace(codePoint)) break;
188        }
189        int nonWhitespaceEnd = len;
190        for (; nonWhitespaceEnd > 0;
191                nonWhitespaceEnd = mStringBefore.offsetByCodePoints(nonWhitespaceEnd, -1)) {
192            final int codePoint = mStringBefore.codePointBefore(nonWhitespaceEnd);
193            if (!Character.isWhitespace(codePoint)) break;
194        }
195        // If nonWhitespaceStart >= nonWhitespaceEnd, that means the selection contained only
196        // whitespace, so we leave it as is.
197        if ((0 != nonWhitespaceStart || len != nonWhitespaceEnd)
198                && nonWhitespaceStart < nonWhitespaceEnd) {
199            mCursorEndAfter = mCursorStartBefore + nonWhitespaceEnd;
200            mCursorStartBefore = mCursorStartAfter = mCursorStartBefore + nonWhitespaceStart;
201            mStringAfter = mStringBefore =
202                    mStringBefore.substring(nonWhitespaceStart, nonWhitespaceEnd);
203        }
204    }
205
206    public String getRecapitalizedString() {
207        return mStringAfter;
208    }
209
210    public int getNewCursorStart() {
211        return mCursorStartAfter;
212    }
213
214    public int getNewCursorEnd() {
215        return mCursorEndAfter;
216    }
217
218    public int getCurrentMode() {
219        return ROTATION_STYLE[mRotationStyleCurrentIndex];
220    }
221}
222