/*
* Copyright (c) 2013 Menny Even-Danan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.anysoftkeyboard;
import com.anysoftkeyboard.dictionaries.content.AndroidUserDictionary;
import com.anysoftkeyboard.keyboards.views.AnyKeyboardBaseView;
import com.anysoftkeyboard.utils.Log;
import com.menny.android.anysoftkeyboard.FeaturesSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* A place to store the currently composing word with information such as adjacent key codes as well
*/
public class WordComposer {
private static final String CHEWBACCAONTHEDRUMS = "chewbacca";
private static final String TAG = "ASK _WC";
/**
* The list of unicode values for each keystroke (including surrounding keys)
*/
private final ArrayList<int[]> mCodes = new ArrayList<>(AndroidUserDictionary.MAX_WORD_LENGTH);
/**
* This holds arrays for reuse. Will not exceed AndroidUserDictionary.MAX_WORD_LENGTH
*/
private final List<int[]> mArraysToReuse = new ArrayList<>(AndroidUserDictionary.MAX_WORD_LENGTH);
/**
* The word chosen from the candidate list, until it is committed.
*/
private CharSequence mPreferredWord;
private final StringBuilder mTypedWord = new StringBuilder(AndroidUserDictionary.MAX_WORD_LENGTH);
private int mCursorPosition;
private int mGlobalCursorPosition;
private int mCapsCount;
private boolean mAutoCapitalized;
/**
* Whether the user chose to capitalize the first char of the word.
*/
private boolean mIsFirstCharCapitalized;
public WordComposer() {
}
/*
WordComposer(WordComposer copy) {
mCodes = new ArrayList<int[]>(copy.mCodes);
mPreferredWord = copy.mPreferredWord;
mTypedWord = new StringBuilder(copy.mTypedWord);
mCapsCount = copy.mCapsCount;
mAutoCapitalized = copy.mAutoCapitalized;
mIsFirstCharCapitalized = copy.mIsFirstCharCapitalized;
}
*/
/**
* Clear out the keys registered so far.
*/
public void reset() {
//moving arrays back to re-use list
for (int[] array : mCodes) {
mArraysToReuse.add(array);
}
mCodes.clear();
mIsFirstCharCapitalized = false;
mPreferredWord = null;
mTypedWord.setLength(0);
mCapsCount = 0;
mCursorPosition = 0;
mGlobalCursorPosition = 0;
}
/**
* Number of keystrokes in the composing word.
*
* @return the number of keystrokes
*/
public int length() {
return mTypedWord.length();
}
/**
* Cursor position
*/
public int cursorPosition() {
return mCursorPosition;
}
public int globalCursorPosition() {
return mGlobalCursorPosition;
}
public void setGlobalCursorPosition(int position) {
mGlobalCursorPosition = position;
}
public boolean setCursorPostion(int position/*, int candidatesStartPosition*/) {
if (position < 0 || position > length())//note: the cursor can be AFTER the word, so it can be equal to size()
{
Log.w(TAG, "New cursor position is invalid! It is outside the word (size " + length() + ", new position " + position + ". Disregarding!!!!");
return false;
}
final boolean changed = mCursorPosition != position;
mCursorPosition = position;
return changed;
//mCandidatesStartPosition = candidatesStartPosition;
}
/*
public boolean hasUserMovedCursor(int cursorPosition)
{
if (AnyApplication.DEBUG)
{
Log.d(TAG, "Current cursor position inside word is "+mCursorPosition+", and word starts at "+mCandidatesStartPosition+". Input's cursor is at "+cursorPosition);
}
return (cursorPosition != (mCursorPosition + mCandidatesStartPosition));
}
public boolean hasUserMovedCursorInsideOfWord(int cursorPosition)
{
if (AnyApplication.DEBUG)
{
Log.d(TAG, "Current word length is "+mTypedWord.length()+", and word starts at "+mCandidatesStartPosition+". Input's cursor is at "+cursorPosition);
}
return (cursorPosition >= mCandidatesStartPosition && cursorPosition <= (mCandidatesStartPosition+mTypedWord.length()));
}
*/
/**
* Returns the codes at a particular position in the word.
*
* @param index the position in the word
* @return the unicode for the pressed and surrounding keys
*/
public int[] getCodesAt(int index) {
return mCodes.get(index);
}
/**
* Add a new keystroke, with codes[0] containing the pressed key's unicode and the rest of
* the array containing unicode for adjacent keys, sorted by reducing probability/proximity.
*
* @param codes the array of unicode values
*/
public boolean add(int primaryCode, int[] codes) {
mTypedWord.insert(mCursorPosition, (char) primaryCode);
/*if (codes != null)
{
for(int i=0; i<codes.length; i++)
{
if (codes[i] > 32) codes[i] = Character.toLowerCase(codes[i]);
}
}*/
correctPrimaryJuxtapos(primaryCode, codes);
//this will return a copy of the codes array, stored in an array with sufficent storage
int[] reusableArray = getReusableArray(codes);
mCodes.add(mCursorPosition, reusableArray);
mCursorPosition++;
if (Character.isUpperCase((char) primaryCode)) mCapsCount++;
if (mTypedWord.length() == CHEWBACCAONTHEDRUMS.length()) {
if (mTypedWord.toString().equalsIgnoreCase(CHEWBACCAONTHEDRUMS)) {
return true;
}
}
return false;
}
private int[] getReusableArray(int[] codes) {
while (mArraysToReuse.size() > 0) {
int[] possibleArray = mArraysToReuse.remove(0);
//is it usable in this situation?
if (possibleArray.length >= codes.length) {
System.arraycopy(codes, 0, possibleArray, 0, codes.length);
if (possibleArray.length > codes.length)
Arrays.fill(possibleArray, codes.length, possibleArray.length, AnyKeyboardBaseView.NOT_A_KEY);
Log.d(TAG, "Found an array to reuse with length " + possibleArray.length);
return possibleArray;
}
}
//if I got here, it means that the reusableArray does not contain a long enough array
Log.d(TAG, "Creating a new array with length " + codes.length);
int[] newArray = new int[codes.length];
mArraysToReuse.add(newArray);
return getReusableArray(codes);
}
/**
* Swaps the first and second values in the codes array if the primary code is not the first
* value in the array but the second. This happens when the preferred key is not the key that
* the user released the finger on.
*
* @param primaryCode the preferred character
* @param nearByKeyCodes array of codes based on distance from touch point
*/
private static void correctPrimaryJuxtapos(int primaryCode, int[] nearByKeyCodes) {
/*if (codes.length < 2) return;
if (codes[0] > 0 && codes[1] > 0 && codes[0] != primaryCode && codes[1] == primaryCode) {
codes[1] = codes[0];
codes[0] = primaryCode;
}*/
if (nearByKeyCodes != null && nearByKeyCodes.length > 1 && primaryCode != nearByKeyCodes[0] && primaryCode != Character.toLowerCase((char) nearByKeyCodes[0])) {
int swapedItem = nearByKeyCodes[0];
nearByKeyCodes[0] = primaryCode;
boolean found = false;
for (int i = 1; i < nearByKeyCodes.length; i++) {
if (nearByKeyCodes[i] == primaryCode) {
nearByKeyCodes[i] = swapedItem;
found = true;
break;
}
}
if (!found) //reverting
nearByKeyCodes[0] = swapedItem;
}
}
/**
* Delete the last keystroke as a result of hitting backspace.
*/
public void deleteLast() {
if (mCursorPosition > 0) {
//removing from the codes list, and taking it back to the reusable list
mArraysToReuse.add(mCodes.remove(mCursorPosition - 1));
//final int lastPos = mTypedWord.length() - 1;
char last = mTypedWord.charAt(mCursorPosition - 1);
mTypedWord.deleteCharAt(mCursorPosition - 1);
mCursorPosition--;
if (Character.isUpperCase(last)) mCapsCount--;
}
}
/**
* Returns the word as it was typed, without any correction applied.
*
* @return the word that was typed so far
*/
public CharSequence getTypedWord() {
int wordSize = mCodes.size();
if (wordSize == 0) {
return "";
}
return mTypedWord;
}
public void setFirstCharCapitalized(boolean capitalized) {
mIsFirstCharCapitalized = capitalized;
}
/**
* Whether or not the user typed a capital letter as the first letter in the word
*
* @return capitalization preference
*/
public boolean isFirstCharCapitalized() {
return mIsFirstCharCapitalized;
}
/**
* Whether or not all of the user typed chars are upper case
*
* @return true if all user typed chars are upper case, false otherwise
*/
public boolean isAllUpperCase() {
return (mCapsCount > 0) && (mCapsCount == length());
}
/**
* Stores the user's selected word, before it is actually committed to the text field.
*
* @param preferred
*/
public void setPreferredWord(CharSequence preferred) {
mPreferredWord = preferred;
}
public CharSequence getPreferredWord() {
return mPreferredWord;
}
/**
* Returns true if more than one character is upper case, otherwise returns false.
*/
public boolean isMostlyCaps() {
return mCapsCount > 1;
}
/**
* Saves the reason why the word is capitalized - whether it was automatic or
* due to the user hitting shift in the middle of a sentence.
*
* @param auto whether it was an automatic capitalization due to start of sentence
*/
public void setAutoCapitalized(boolean auto) {
mAutoCapitalized = auto;
}
/**
* Returns whether the word was automatically capitalized.
*
* @return whether the word was automatically capitalized
*/
public boolean isAutoCapitalized() {
return mAutoCapitalized;
}
public void logCodes() {
if (!FeaturesSet.DEBUG_LOG) return;
Log.d(TAG, "Word: " + mTypedWord + ", prefered word:" + mPreferredWord);
int i = 0;
for (int[] codes : mCodes) {
String codesString = "Codes #" + i + ": ";
for (int c : codes) {
codesString += "" + c + ",";
}
Log.d(TAG, codesString);
}
}
}