diff --git a/src/ui/android/AndroidManifest.xml b/src/ui/android/AndroidManifest.xml index d9f983e65..47a0073c7 100644 --- a/src/ui/android/AndroidManifest.xml +++ b/src/ui/android/AndroidManifest.xml @@ -25,6 +25,13 @@ + + + + + + diff --git a/src/ui/android/res/drawable-hdpi/btn_close.png b/src/ui/android/res/drawable-hdpi/btn_close.png new file mode 100644 index 000000000..47f11e5bf Binary files /dev/null and b/src/ui/android/res/drawable-hdpi/btn_close.png differ diff --git a/src/ui/android/res/drawable-hdpi/sym_keyboard_delete.png b/src/ui/android/res/drawable-hdpi/sym_keyboard_delete.png new file mode 100644 index 000000000..5139c7179 Binary files /dev/null and b/src/ui/android/res/drawable-hdpi/sym_keyboard_delete.png differ diff --git a/src/ui/android/res/drawable-hdpi/sym_keyboard_return.png b/src/ui/android/res/drawable-hdpi/sym_keyboard_return.png new file mode 100644 index 000000000..5a5670c32 Binary files /dev/null and b/src/ui/android/res/drawable-hdpi/sym_keyboard_return.png differ diff --git a/src/ui/android/res/drawable-hdpi/sym_keyboard_search.png b/src/ui/android/res/drawable-hdpi/sym_keyboard_search.png new file mode 100644 index 000000000..e72cde3bb Binary files /dev/null and b/src/ui/android/res/drawable-hdpi/sym_keyboard_search.png differ diff --git a/src/ui/android/res/drawable-hdpi/sym_keyboard_shift.png b/src/ui/android/res/drawable-hdpi/sym_keyboard_shift.png new file mode 100644 index 000000000..275769618 Binary files /dev/null and b/src/ui/android/res/drawable-hdpi/sym_keyboard_shift.png differ diff --git a/src/ui/android/res/drawable-hdpi/sym_keyboard_space.png b/src/ui/android/res/drawable-hdpi/sym_keyboard_space.png new file mode 100644 index 000000000..cef2daa5d Binary files /dev/null and b/src/ui/android/res/drawable-hdpi/sym_keyboard_space.png differ diff --git a/src/ui/android/res/drawable-mdpi/sym_keyboard_delete.png b/src/ui/android/res/drawable-mdpi/sym_keyboard_delete.png new file mode 100644 index 000000000..6cee59682 Binary files /dev/null and b/src/ui/android/res/drawable-mdpi/sym_keyboard_delete.png differ diff --git a/src/ui/android/res/drawable-mdpi/sym_keyboard_done.png b/src/ui/android/res/drawable-mdpi/sym_keyboard_done.png new file mode 100644 index 000000000..c0d6d1394 Binary files /dev/null and b/src/ui/android/res/drawable-mdpi/sym_keyboard_done.png differ diff --git a/src/ui/android/res/drawable-mdpi/sym_keyboard_return.png b/src/ui/android/res/drawable-mdpi/sym_keyboard_return.png new file mode 100644 index 000000000..cbe2b152f Binary files /dev/null and b/src/ui/android/res/drawable-mdpi/sym_keyboard_return.png differ diff --git a/src/ui/android/res/drawable-mdpi/sym_keyboard_search.png b/src/ui/android/res/drawable-mdpi/sym_keyboard_search.png new file mode 100644 index 000000000..127755d6b Binary files /dev/null and b/src/ui/android/res/drawable-mdpi/sym_keyboard_search.png differ diff --git a/src/ui/android/res/drawable-mdpi/sym_keyboard_shift.png b/src/ui/android/res/drawable-mdpi/sym_keyboard_shift.png new file mode 100644 index 000000000..d05962846 Binary files /dev/null and b/src/ui/android/res/drawable-mdpi/sym_keyboard_shift.png differ diff --git a/src/ui/android/res/drawable-mdpi/sym_keyboard_space.png b/src/ui/android/res/drawable-mdpi/sym_keyboard_space.png new file mode 100644 index 000000000..09b94d9e6 Binary files /dev/null and b/src/ui/android/res/drawable-mdpi/sym_keyboard_space.png differ diff --git a/src/ui/android/res/layout/input.xml b/src/ui/android/res/layout/input.xml new file mode 100644 index 000000000..fdef07a53 --- /dev/null +++ b/src/ui/android/res/layout/input.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/ui/android/res/layout/keyboard_languages_options.xml b/src/ui/android/res/layout/keyboard_languages_options.xml new file mode 100644 index 000000000..0b45b739c --- /dev/null +++ b/src/ui/android/res/layout/keyboard_languages_options.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/src/ui/android/res/values/dimens.xml b/src/ui/android/res/values/dimens.xml index 55c1e5908..55088756c 100644 --- a/src/ui/android/res/values/dimens.xml +++ b/src/ui/android/res/values/dimens.xml @@ -3,5 +3,8 @@ 16dp 16dp + 50dip + 16sp + 6sp diff --git a/src/ui/android/res/values/strings.xml b/src/ui/android/res/values/strings.xml index 57e20027d..e7c4e560f 100644 --- a/src/ui/android/res/values/strings.xml +++ b/src/ui/android/res/values/strings.xml @@ -9,4 +9,15 @@ Speech Input Keyboard Input org.grammaticalframework.ui.android.GLOBAL_PREFERENCES + + + Done + Go + Next + Previous + Send + + + normalKeyboardMode + internalKeyboardMode diff --git a/src/ui/android/res/xml/cyrillic.xml b/src/ui/android/res/xml/cyrillic.xml new file mode 100644 index 000000000..2e444507b --- /dev/null +++ b/src/ui/android/res/xml/cyrillic.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ui/android/res/xml/method.xml b/src/ui/android/res/xml/method.xml new file mode 100644 index 000000000..af83761ca --- /dev/null +++ b/src/ui/android/res/xml/method.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/ui/android/res/xml/popup_keyboard.xml b/src/ui/android/res/xml/popup_keyboard.xml new file mode 100644 index 000000000..521d3d278 --- /dev/null +++ b/src/ui/android/res/xml/popup_keyboard.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/ui/android/res/xml/qwerty.xml b/src/ui/android/res/xml/qwerty.xml new file mode 100644 index 000000000..6e0b95975 --- /dev/null +++ b/src/ui/android/res/xml/qwerty.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ui/android/res/xml/symbols.xml b/src/ui/android/res/xml/symbols.xml new file mode 100644 index 000000000..72deff01c --- /dev/null +++ b/src/ui/android/res/xml/symbols.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ui/android/res/xml/symbols_shift.xml b/src/ui/android/res/xml/symbols_shift.xml new file mode 100644 index 000000000..b55e6f521 --- /dev/null +++ b/src/ui/android/res/xml/symbols_shift.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/ConversationView.java b/src/ui/android/src/org/grammaticalframework/ui/android/ConversationView.java index cda719604..0a1004462 100644 --- a/src/ui/android/src/org/grammaticalframework/ui/android/ConversationView.java +++ b/src/ui/android/src/org/grammaticalframework/ui/android/ConversationView.java @@ -1,6 +1,7 @@ package org.grammaticalframework.ui.android; import android.content.Context; +import android.os.Bundle; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -88,6 +89,9 @@ public class ConversationView extends ScrollView { return false; } }); + Bundle extras = edittext.getInputExtras(true); + extras.putBoolean("show_language_toggle", false); + mContent.addView(view); post(new Runnable() { public void run() { diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/Language.java b/src/ui/android/src/org/grammaticalframework/ui/android/Language.java index 996846e82..6b63bb8f2 100644 --- a/src/ui/android/src/org/grammaticalframework/ui/android/Language.java +++ b/src/ui/android/src/org/grammaticalframework/ui/android/Language.java @@ -9,12 +9,15 @@ public class Language implements Serializable { private final String mLangName; private final String mConcrete; private final int mInflResource; + private final int mKeyboardResource; - public Language(String langCode, String langName, String concrete, int inflResource) { + public Language(String langCode, String langName, String concrete, + int inflResource, int keyboardResource) { mLangCode = langCode; mLangName = langName; mConcrete = concrete; mInflResource = inflResource; + mKeyboardResource = keyboardResource; } public String getLangCode() { @@ -28,6 +31,10 @@ public class Language implements Serializable { public int getInflectionResource() { return mInflResource; } + + public int getKeyboardResource() { + return mKeyboardResource; + } String getConcrete() { return mConcrete; diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/LexicalEntryActivity.java b/src/ui/android/src/org/grammaticalframework/ui/android/LexicalEntryActivity.java index 8c8777e2f..8d9a41955 100644 --- a/src/ui/android/src/org/grammaticalframework/ui/android/LexicalEntryActivity.java +++ b/src/ui/android/src/org/grammaticalframework/ui/android/LexicalEntryActivity.java @@ -45,7 +45,7 @@ public class LexicalEntryActivity extends ListActivity { mTranslator = ((GFTranslator) getApplicationContext()).getTranslator(); mShowLanguageView = (LanguageSelector) findViewById(R.id.show_language); - mShowLanguageView.setLanguages(mTranslator.getAvailableSourceLanguages()); + mShowLanguageView.setLanguages(mTranslator.getAvailableLanguages()); mShowLanguageView.setOnLanguageSelectedListener(new OnLanguageSelectedListener() { @Override public void onLanguageSelected(Language language) { diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/MainActivity.java b/src/ui/android/src/org/grammaticalframework/ui/android/MainActivity.java index ab14bbfe2..9cbeb6930 100644 --- a/src/ui/android/src/org/grammaticalframework/ui/android/MainActivity.java +++ b/src/ui/android/src/org/grammaticalframework/ui/android/MainActivity.java @@ -101,14 +101,14 @@ public class MainActivity extends Activity { mTranslator = ((GFTranslator) getApplicationContext()).getTranslator(); - mSourceLanguageView.setLanguages(mTranslator.getAvailableSourceLanguages()); + mSourceLanguageView.setLanguages(mTranslator.getAvailableLanguages()); mSourceLanguageView.setOnLanguageSelectedListener(new OnLanguageSelectedListener() { @Override public void onLanguageSelected(Language language) { onSourceLanguageSelected(language); } }); - mTargetLanguageView.setLanguages(mTranslator.getAvailableTargetLanguages()); + mTargetLanguageView.setLanguages(mTranslator.getAvailableLanguages()); mTargetLanguageView.setOnLanguageSelectedListener(new OnLanguageSelectedListener() { @Override public void onLanguageSelected(Language language) { @@ -192,10 +192,16 @@ public class MainActivity extends Activity { void onSourceLanguageSelected(Language language) { mTranslator.setSourceLanguage(language); + if (TranslatorInputMethodService.getInstance() != null) { + TranslatorInputMethodService.getInstance().handleChangeSourceLanguage(language); + } } void onTargetLanguageSelected(Language language) { mTranslator.setTargetLanguage(language); + if (TranslatorInputMethodService.getInstance() != null) { + TranslatorInputMethodService.getInstance().handleChangeTargetLanguage(language); + } } public String getSourceLanguageCode() { @@ -211,6 +217,10 @@ public class MainActivity extends Activity { Language newTarget = mTranslator.getSourceLanguage(); mSourceLanguageView.setSelectedLanguage(newSource); mTargetLanguageView.setSelectedLanguage(newTarget); + + if (TranslatorInputMethodService.getInstance() != null) { + TranslatorInputMethodService.getInstance().handleSwitchLanguages(); + } } private void startRecognition() { diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/Translator.java b/src/ui/android/src/org/grammaticalframework/ui/android/Translator.java index f3a247844..e8ef4738e 100644 --- a/src/ui/android/src/org/grammaticalframework/ui/android/Translator.java +++ b/src/ui/android/src/org/grammaticalframework/ui/android/Translator.java @@ -33,8 +33,9 @@ public class Translator { new Language("fi-FI", "Finnish", "TranslateFin", 0), new Language("sv-SE", "Swedish", "TranslateSwe", R.xml.inflection_sv), */ - new Language("en-US", "English", "ParseEng", R.xml.inflection_en), - new Language("bg-BG", "Bulgarian", "ParseBul", R.xml.inflection_bg), + new Language("en-US", "English", "ParseEng", R.xml.inflection_en, R.xml.qwerty), + new Language("bg-BG", "Bulgarian", "ParseBul", R.xml.inflection_bg, R.xml.cyrillic), + new Language("sv-SE", "Swedish", "ParseSwe", R.xml.inflection_sv, R.xml.qwerty), }; private Language mSourceLanguage; @@ -78,11 +79,7 @@ public class Translator { mTargetLanguage = getPrefLang(TARGET_LANG_KEY, 1); } - public List getAvailableSourceLanguages() { - return Arrays.asList(mLanguages); - } - - public List getAvailableTargetLanguages() { + public List getAvailableLanguages() { return Arrays.asList(mLanguages); } diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/TranslatorInputMethodService.java b/src/ui/android/src/org/grammaticalframework/ui/android/TranslatorInputMethodService.java new file mode 100644 index 000000000..29c52c44a --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/TranslatorInputMethodService.java @@ -0,0 +1,581 @@ +package org.grammaticalframework.ui.android; + +import android.inputmethodservice.InputMethodService; +import android.text.InputType; +import android.text.method.MetaKeyKeyListener; +import android.util.Log; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + +import java.util.ArrayList; +import java.util.List; + + +public class TranslatorInputMethodService extends InputMethodService + implements android.inputmethodservice.KeyboardView.OnKeyboardActionListener { + + private TranslatorKeyboardView mInputView; + private CompletionsView mCandidateView; + private CompletionInfo[] mCompletions; + + private StringBuilder mComposing = new StringBuilder(); + private boolean mPredictionOn; + private boolean mCompletionOn; + private boolean mCapsLock; + private long mLastShiftTime; + private long mMetaState; + + private TranslatorKeyboard mSymbolsKeyboard; + private TranslatorKeyboard mSymbolsShiftedKeyboard; + private TranslatorKeyboard mLanguageKeyboard; + + private TranslatorKeyboard mCurKeyboard; + + private int mActionId; + + private Translator mTranslator; + + @Override + public void onCreate() { + super.onCreate(); + + mTranslator = ((GFTranslator) getApplicationContext()).getTranslator(); + + mSymbolsKeyboard = null; + mSymbolsShiftedKeyboard = null; + mLanguageKeyboard = null; + } + + @Override + public View onCreateInputView() { + mInputView = (TranslatorKeyboardView) + getLayoutInflater().inflate(R.layout.input, null); + mInputView.setOnKeyboardActionListener(this); + mInputView.setKeyboard(mCurKeyboard); + return mInputView; + } + + @Override + public View onCreateCandidatesView() { + mCandidateView = new CompletionsView(this); + mCandidateView.setService(this); + return mCandidateView; + } + + private int mModeId; + private static TranslatorInputMethodService mInstance; + + static TranslatorInputMethodService getInstance() { + return mInstance; + } + + @Override + public void onStartInput(EditorInfo attribute, boolean restarting) { + super.onStartInput(attribute, restarting); + + // Reset our state. We want to do this even if restarting, because + // the underlying state of the text editor could have changed in any way. + mComposing.setLength(0); + updateCandidates(); + + if (!restarting) { + // Clear shift states. + mMetaState = 0; + } + + mPredictionOn = false; + mCompletionOn = false; + mCompletions = null; + + int res = + mTranslator.getSourceLanguage().getKeyboardResource(); + mModeId = R.string.normalKeyboardMode; + if (attribute.extras != null && + !attribute.extras.getBoolean("show_language_toggle", true)) { + mModeId = R.string.internalKeyboardMode; + } + mLanguageKeyboard = new TranslatorKeyboard(this, res, mModeId); + mSymbolsKeyboard = new TranslatorKeyboard(this, R.xml.symbols, mModeId); + mSymbolsShiftedKeyboard = new TranslatorKeyboard(this, R.xml.symbols_shift, mModeId); + + // We are now going to initialize our state based on the type of + // text being edited. + switch (attribute.inputType & InputType.TYPE_MASK_CLASS) { + case InputType.TYPE_CLASS_NUMBER: + case InputType.TYPE_CLASS_DATETIME: + // Numbers and dates default to the symbols keyboard, with + // no extra features. + mCurKeyboard = mSymbolsKeyboard; + break; + + case InputType.TYPE_CLASS_PHONE: + // Phones will also default to the symbols keyboard, though + // often you will want to have a dedicated phone keyboard. + mCurKeyboard = mSymbolsKeyboard; + break; + + case InputType.TYPE_CLASS_TEXT: + // This is general text editing. We will default to the + // normal alphabetic keyboard, and assume that we should + // be doing predictive text (showing candidates as the + // user types). + mCurKeyboard = mLanguageKeyboard; + mPredictionOn = true; + + // We now look for a few special variations of text that will + // modify our behavior. + int variation = attribute.inputType & InputType.TYPE_MASK_VARIATION; + if (variation == InputType.TYPE_TEXT_VARIATION_PASSWORD || + variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) { + // Do not display predictions / what the user is typing + // when they are entering a password. + mPredictionOn = false; + } + + if (variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + || variation == InputType.TYPE_TEXT_VARIATION_URI + || variation == InputType.TYPE_TEXT_VARIATION_FILTER) { + // Our predictions are not useful for e-mail addresses + // or URIs. + mPredictionOn = false; + } + + if ((attribute.inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) { + // If this is an auto-complete text view, then our predictions + // will not be shown and instead we will allow the editor + // to supply their own. We only show the editor's + // candidates when in full-screen mode, otherwise relying + // own it displaying its own UI. + mPredictionOn = false; + mCompletionOn = isFullscreenMode(); + } + + // We also want to look at the current state of the editor + // to decide whether our alphabetic keyboard should start out + // shifted. + updateShiftKeyState(attribute); + break; + + default: + // For all unknown input types, default to the alphabetic + // keyboard with no special features. + mCurKeyboard = mLanguageKeyboard; + updateShiftKeyState(attribute); + } + + mActionId = attribute.imeOptions & EditorInfo.IME_MASK_ACTION; + mCurKeyboard.setImeOptions(getResources(), attribute.imeOptions); + + mInstance = this; + } + + @Override + public void onFinishInput() { + super.onFinishInput(); + + // Clear current composing text and candidates. + mComposing.setLength(0); + updateCandidates(); + + // We only hide the candidates window when finishing input on + // a particular editor, to avoid popping the underlying application + // up and down if the user is entering text into the bottom of + // its window. + setCandidatesViewShown(false); + + mCurKeyboard = mLanguageKeyboard; + if (mInputView != null) { + mInputView.closing(); + } + + mInstance = null; + } + + @Override + public void onStartInputView(EditorInfo attribute, boolean restarting) { + super.onStartInputView(attribute, restarting); + // Apply the selected keyboard to the input view. + mInputView.setKeyboard(mCurKeyboard); + mInputView.closing(); + } + + @Override + public void onUpdateSelection(int oldSelStart, int oldSelEnd, + int newSelStart, int newSelEnd, + int candidatesStart, int candidatesEnd) { + super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, + candidatesStart, candidatesEnd); + + // If the current selection in the text view changes, we should + // clear whatever candidate text we have. + if (mComposing.length() > 0 && (newSelStart != candidatesEnd + || newSelEnd != candidatesEnd)) { + mComposing.setLength(0); + updateCandidates(); + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.finishComposingText(); + } + } + } + + @Override + public void onDisplayCompletions(CompletionInfo[] completions) { + if (mCompletionOn) { + mCompletions = completions; + if (completions == null) { + setSuggestions(null, false, false); + return; + } + + List stringList = new ArrayList(); + for (int i = 0; i < completions.length; i++) { + CompletionInfo ci = completions[i]; + if (ci != null) stringList.add(ci.getText().toString()); + } + setSuggestions(stringList, true, true); + } + } + + /** + * This translates incoming hard key events in to edit operations on an + * InputConnection. It is only needed when using the + * PROCESS_HARD_KEYS option. + */ + private boolean translateKeyDown(int keyCode, KeyEvent event) { + mMetaState = MetaKeyKeyListener.handleKeyDown(mMetaState, + keyCode, event); + int c = event.getUnicodeChar(MetaKeyKeyListener.getMetaState(mMetaState)); + mMetaState = MetaKeyKeyListener.adjustMetaAfterKeypress(mMetaState); + InputConnection ic = getCurrentInputConnection(); + if (c == 0 || ic == null) { + return false; + } + + if ((c & KeyCharacterMap.COMBINING_ACCENT) != 0) { + c = c & KeyCharacterMap.COMBINING_ACCENT_MASK; + } + + if (mComposing.length() > 0) { + char accent = mComposing.charAt(mComposing.length() -1 ); + int composed = KeyEvent.getDeadChar(accent, c); + + if (composed != 0) { + c = composed; + mComposing.setLength(mComposing.length()-1); + } + } + + onKey(c, null); + + return true; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + // The InputMethodService already takes care of the back + // key for us, to dismiss the input method if it is shown. + // However, our keyboard could be showing a pop-up window + // that back should dismiss, so we first allow it to do that. + if (event.getRepeatCount() == 0 && mInputView != null) { + if (mInputView.handleBack()) { + return true; + } + } + break; + + case KeyEvent.KEYCODE_DEL: + // Special handling of the delete key: if we currently are + // composing text for the user, we want to modify that instead + // of let the application to the delete itself. + if (mComposing.length() > 0) { + onKey(TranslatorKeyboard.KEYCODE_DELETE, null); + return true; + } + break; + + case KeyEvent.KEYCODE_ENTER: + // Let the underlying text editor always handle these. + return false; + + default: + if (mPredictionOn && translateKeyDown(keyCode, event)) { + return true; + } + } + + return super.onKeyDown(keyCode, event); + } + + /** + * Helper function to commit any text being composed in to the editor. + */ + private void commitTyped(InputConnection inputConnection) { + if (mComposing.length() > 0) { + inputConnection.commitText(mComposing, mComposing.length()); + mComposing.setLength(0); + updateCandidates(); + } + } + + /** + * Helper to update the shift state of our keyboard based on the initial + * editor state. + */ + private void updateShiftKeyState(EditorInfo attr) { + if (attr != null + && mInputView != null && mLanguageKeyboard == mInputView.getKeyboard()) { + int caps = 0; + EditorInfo ei = getCurrentInputEditorInfo(); + if (ei != null && ei.inputType != InputType.TYPE_NULL) { + caps = getCurrentInputConnection().getCursorCapsMode(attr.inputType); + } + mInputView.setShifted(mCapsLock || caps != 0); + } + } + + /** + * Helper to send a key down / key up pair to the current editor. + */ + private void keyDownUp(int keyEventCode) { + getCurrentInputConnection().sendKeyEvent( + new KeyEvent(KeyEvent.ACTION_DOWN, keyEventCode)); + getCurrentInputConnection().sendKeyEvent( + new KeyEvent(KeyEvent.ACTION_UP, keyEventCode)); + } + + // Implementation of KeyboardViewListener + public void onKey(int primaryCode, int[] keyCodes) { + if (primaryCode == TranslatorKeyboard.KEYCODE_DELETE) { + handleBackspace(); + } else if (primaryCode == TranslatorKeyboard.KEYCODE_SHIFT) { + handleShift(); + } else if (primaryCode == TranslatorKeyboard.KEYCODE_SOURCE_LANGUAGE + && mInputView != null) { + Language newSource = mTranslator.getTargetLanguage(); + Language newTarget = mTranslator.getSourceLanguage(); + mTranslator.setSourceLanguage(newSource); + mTranslator.setTargetLanguage(newTarget); + handleSwitchLanguages(); + } else if (primaryCode < TranslatorKeyboard.KEYCODE_SOURCE_LANGUAGE && + primaryCode > TranslatorKeyboard.KEYCODE_SOURCE_LANGUAGE-TranslatorKeyboard.MAX_LANGUAGE_KEYCODES) { + Language newSource = + mTranslator.getAvailableLanguages().get(TranslatorKeyboard.KEYCODE_SOURCE_LANGUAGE-primaryCode-1); + mTranslator.setSourceLanguage(newSource); + handleChangeSourceLanguage(newSource); + } else if (primaryCode == TranslatorKeyboard.KEYCODE_TARGET_LANGUAGE) { + String translation = mTranslator.translate(mComposing.toString()); + getCurrentInputConnection().commitText(translation, 1); + return; + } else if (primaryCode < TranslatorKeyboard.KEYCODE_TARGET_LANGUAGE && + primaryCode > TranslatorKeyboard.KEYCODE_TARGET_LANGUAGE-TranslatorKeyboard.MAX_LANGUAGE_KEYCODES) { + Language newTarget = + mTranslator.getAvailableLanguages().get(TranslatorKeyboard.KEYCODE_TARGET_LANGUAGE-primaryCode-1); + mTranslator.setTargetLanguage(newTarget); + handleChangeTargetLanguage(newTarget); + } else if (primaryCode == TranslatorKeyboard.KEYCODE_MODE_CHANGE + && mInputView != null) { + TranslatorKeyboard current = (TranslatorKeyboard) mInputView.getKeyboard(); + if (current == mSymbolsKeyboard || current == mSymbolsShiftedKeyboard) { + current = mLanguageKeyboard; + } else { + current = mSymbolsKeyboard; + } + mInputView.setKeyboard(current); + if (current == mSymbolsKeyboard) { + current.setShifted(false); + } + } else if (primaryCode == 10) { + getCurrentInputConnection().performEditorAction(mActionId); + } else if (primaryCode == ' ' && mComposing.length() == 0) { + getCurrentInputConnection().commitText(" ", 1); + } else { + handleCharacter(primaryCode, keyCodes); + } + } + + public void onText(CharSequence text) { + InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; + ic.beginBatchEdit(); + if (mComposing.length() > 0) { + commitTyped(ic); + } + ic.commitText(text, 0); + ic.endBatchEdit(); + updateShiftKeyState(getCurrentInputEditorInfo()); + } + + /** + * Update the list of available candidates from the current composing + * text. This will need to be filled in by however you are determining + * candidates. + */ + private void updateCandidates() { + if (!mCompletionOn) { + if (mComposing.length() > 0) { + ArrayList list = new ArrayList(); + list.add(mComposing.toString()); + list.add("alfa"); + list.add("beta"); + setSuggestions(list, true, true); + Log.d("KEYBOARD", mComposing.toString()); + } else { + setSuggestions(null, false, false); + } + } + } + + public void setSuggestions(List suggestions, boolean completions, + boolean typedWordValid) { + if (suggestions != null && suggestions.size() > 0) { + setCandidatesViewShown(true); + } else if (isExtractViewShown()) { + setCandidatesViewShown(true); + } + if (mCandidateView != null) { + mCandidateView.setSuggestions(suggestions, completions, typedWordValid); + } + } + + private void handleBackspace() { + final int length = mComposing.length(); + if (length > 1) { + mComposing.delete(length - 1, length); + getCurrentInputConnection().setComposingText(mComposing, 1); + updateCandidates(); + } else if (length > 0) { + mComposing.setLength(0); + getCurrentInputConnection().commitText("", 0); + updateCandidates(); + } else { + keyDownUp(KeyEvent.KEYCODE_DEL); + } + updateShiftKeyState(getCurrentInputEditorInfo()); + } + + private void handleShift() { + if (mInputView == null) { + return; + } + + TranslatorKeyboard currentKeyboard = (TranslatorKeyboard) mInputView.getKeyboard(); + if (mLanguageKeyboard == currentKeyboard) { + // Alphabet keyboard + checkToggleCapsLock(); + mInputView.setShifted(mCapsLock || !mInputView.isShifted()); + } else if (currentKeyboard == mSymbolsKeyboard) { + mSymbolsKeyboard.setShifted(true); + mInputView.setKeyboard(mSymbolsShiftedKeyboard); + mSymbolsShiftedKeyboard.setShifted(true); + } else if (currentKeyboard == mSymbolsShiftedKeyboard) { + mSymbolsShiftedKeyboard.setShifted(false); + mInputView.setKeyboard(mSymbolsKeyboard); + mSymbolsKeyboard.setShifted(false); + } + } + + private void handleCharacter(int primaryCode, int[] keyCodes) { + if (isInputViewShown()) { + if (mInputView.isShifted()) { + primaryCode = Character.toUpperCase(primaryCode); + } + } + + mComposing.append((char) primaryCode); + getCurrentInputConnection().setComposingText(mComposing, 1); + updateShiftKeyState(getCurrentInputEditorInfo()); + + if (mPredictionOn) { + updateCandidates(); + } + } + + private void handleClose() { + commitTyped(getCurrentInputConnection()); + requestHideSelf(0); + mInputView.closing(); + } + + void handleChangeSourceLanguage(Language newSource) { + mLanguageKeyboard = + new TranslatorKeyboard(this, newSource.getKeyboardResource(), mModeId); + mSymbolsKeyboard.updateLanguageKeyLabels(); + mSymbolsShiftedKeyboard.updateLanguageKeyLabels(); + mInputView.setKeyboard(mLanguageKeyboard); + } + + void handleChangeTargetLanguage(Language newTarget) { + mLanguageKeyboard.updateLanguageKeyLabels(); + mSymbolsKeyboard.updateLanguageKeyLabels(); + mSymbolsShiftedKeyboard.updateLanguageKeyLabels(); + mInputView.invalidateAllKeys(); + } + + void handleSwitchLanguages() { + Language newSource = mTranslator.getSourceLanguage(); + mLanguageKeyboard = + new TranslatorKeyboard(this, newSource.getKeyboardResource(), mModeId); + mInputView.setKeyboard(mLanguageKeyboard); + } + + private void checkToggleCapsLock() { + long now = System.currentTimeMillis(); + if (mLastShiftTime + 800 > now) { + mCapsLock = !mCapsLock; + mLastShiftTime = 0; + } else { + mLastShiftTime = now; + } + } + + public void pickDefaultCandidate() { + pickSuggestionManually(0); + } + + public void pickSuggestionManually(int index) { + if (mCompletionOn && mCompletions != null && index >= 0 + && index < mCompletions.length) { + CompletionInfo ci = mCompletions[index]; + getCurrentInputConnection().commitCompletion(ci); + if (mCandidateView != null) { + mCandidateView.clear(); + } + updateShiftKeyState(getCurrentInputEditorInfo()); + } else if (mComposing.length() > 0) { + // If we were generating candidate suggestions for the current + // text, we would commit one of them here. But for this sample, + // we will just commit the current text. + commitTyped(getCurrentInputConnection()); + } + } + + public void swipeRight() { + if (mCompletionOn) { + pickDefaultCandidate(); + } + } + + public void swipeLeft() { + handleBackspace(); + } + + public void swipeDown() { + handleClose(); + } + + public void swipeUp() { + } + + public void onPress(int primaryCode) { + } + + public void onRelease(int primaryCode) { + } +} diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/TranslatorKeyboard.java b/src/ui/android/src/org/grammaticalframework/ui/android/TranslatorKeyboard.java new file mode 100644 index 000000000..cd29d6914 --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/TranslatorKeyboard.java @@ -0,0 +1,103 @@ +package org.grammaticalframework.ui.android; + +import java.util.Locale; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.view.inputmethod.EditorInfo; +import android.inputmethodservice.Keyboard; + +public class TranslatorKeyboard extends Keyboard { + + private Key mEnterKey; + private Key mSourceLanguageKey; + private Key mTargetLanguageKey; + + static final int KEYCODE_SOURCE_LANGUAGE = -100; + static final int KEYCODE_TARGET_LANGUAGE = -200; + static final int MAX_LANGUAGE_KEYCODES = 99; + + private Translator mTranslator; + + public TranslatorKeyboard(Context context, int xmlLayoutResId, int modeId) { + super(context, xmlLayoutResId, modeId); + + mTranslator = ((GFTranslator) context.getApplicationContext()).getTranslator(); + updateLanguageKeyLabels(); + } + + public void updateLanguageKeyLabels() { + if (mSourceLanguageKey != null) + mSourceLanguageKey.label = getLanguageKeyLabel(mTranslator.getSourceLanguage()); + + if (mTargetLanguageKey != null) + mTargetLanguageKey.label = getLanguageKeyLabel(mTranslator.getTargetLanguage()); + } + + public static String getLanguageKeyLabel(Language lang) { + return + LocaleUtils.parseJavaLocale(lang.getLangCode(), Locale.getDefault()) + .getISO3Language(); + } + + @Override + protected Key createKeyFromXml(Resources res, Row parent, int x, int y, + XmlResourceParser parser) { + Key key = new Key(res, parent, x, y, parser); + if (key.codes[0] == 10) { + mEnterKey = key; + } else if (key.codes[0] == KEYCODE_SOURCE_LANGUAGE) { + mSourceLanguageKey = key; + } else if (key.codes[0] == KEYCODE_TARGET_LANGUAGE) { + mTargetLanguageKey = key; + } + return key; + } + + /** + * This looks at the ime options given by the current editor, to set the + * appropriate label on the keyboard's enter key (if it has one). + */ + void setImeOptions(Resources res, int options) { + if (mEnterKey == null) { + return; + } + + switch (options&(EditorInfo.IME_MASK_ACTION|EditorInfo.IME_FLAG_NO_ENTER_ACTION)) { + case EditorInfo.IME_ACTION_DONE: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_done_key); + break; + case EditorInfo.IME_ACTION_GO: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_go_key); + break; + case EditorInfo.IME_ACTION_NEXT: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_next_key); + break; + case EditorInfo.IME_ACTION_PREVIOUS: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_previous_key); + break; + case EditorInfo.IME_ACTION_SEARCH: + mEnterKey.icon = res.getDrawable(R.drawable.sym_keyboard_search); + mEnterKey.label = null; + break; + case EditorInfo.IME_ACTION_SEND: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_send_key); + break; + default: + mEnterKey.icon = res.getDrawable(R.drawable.sym_keyboard_return); + mEnterKey.label = null; + break; + } + } +} \ No newline at end of file diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/TranslatorKeyboardView.java b/src/ui/android/src/org/grammaticalframework/ui/android/TranslatorKeyboardView.java new file mode 100644 index 000000000..6bca2ad59 --- /dev/null +++ b/src/ui/android/src/org/grammaticalframework/ui/android/TranslatorKeyboardView.java @@ -0,0 +1,113 @@ +package org.grammaticalframework.ui.android; + +import java.util.Locale; + +import org.grammaticalframework.ui.android.TranslatorKeyboard; + +import android.content.Context; +import android.inputmethodservice.Keyboard.Key; +import android.inputmethodservice.KeyboardView; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.PopupWindow; + +public class TranslatorKeyboardView extends KeyboardView { + + private Translator mTranslator; + + public TranslatorKeyboardView(Context context, AttributeSet attrs) { + super(context, attrs); + mTranslator = ((GFTranslator) context.getApplicationContext()).getTranslator(); + } + + public TranslatorKeyboardView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mTranslator = ((GFTranslator) context.getApplicationContext()).getTranslator(); + } + + private PopupWindow mLanguagesPopup = null; + private Key mLanguagesKey = null; + + private void showLanguageOptions(Key popupKey) { + if (mLanguagesPopup == null) { + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + LinearLayout popupContainer = (LinearLayout) + inflater.inflate(R.layout.keyboard_languages_options, null); + + int index = 0; + for (Language lang : mTranslator.getAvailableLanguages()) { + Button item = new Button(getContext()); + item.setText(TranslatorKeyboard.getLanguageKeyLabel(lang)); + item.setTag(index); + item.setOnClickListener(this); + popupContainer.addView(item, index++); + } + + popupContainer.measure( + MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); + + mLanguagesPopup = new PopupWindow(getContext()); + mLanguagesPopup.setWidth(popupContainer.getMeasuredWidth()); + mLanguagesPopup.setHeight(popupContainer.getMeasuredHeight()); + mLanguagesPopup.setContentView(popupContainer); + + int[] windowOffset = new int[2]; + getLocationInWindow(windowOffset); + int popupX = popupKey.x + popupKey.width - popupContainer.getMeasuredWidth(); + int popupY = popupKey.y - popupContainer.getMeasuredHeight(); + final int x = popupX + popupContainer.getPaddingRight() + windowOffset[0]; + final int y = popupY + popupContainer.getPaddingBottom() + windowOffset[1]; + mLanguagesPopup.showAtLocation(this, Gravity.NO_GRAVITY, x, y); + + View closeButton = popupContainer.findViewById(R.id.closeButton); + if (closeButton != null) closeButton.setOnClickListener(this); + } + + mLanguagesKey = popupKey; + } + + private void dismissLanguages() { + if (mLanguagesPopup != null) { + mLanguagesPopup.dismiss(); + mLanguagesPopup = null; + mLanguagesKey = null; + } + } + + @Override + public void onClick(View v) { + super.onClick(v); + + if (v.getTag() != null) { + if (mLanguagesKey.codes[0] == TranslatorKeyboard.KEYCODE_SOURCE_LANGUAGE || + mLanguagesKey.codes[0] == TranslatorKeyboard.KEYCODE_TARGET_LANGUAGE) { + int keyCode = mLanguagesKey.codes[0] - ((Integer) v.getTag()) - 1; + getOnKeyboardActionListener().onKey(keyCode, new int[] {keyCode}); + } + } + + dismissLanguages(); + } + + public void closing() { + super.closing(); + dismissLanguages(); + } + + @Override + protected boolean onLongPress(Key key) { + if (key.codes[0] == TranslatorKeyboard.KEYCODE_SOURCE_LANGUAGE || + key.codes[0] == TranslatorKeyboard.KEYCODE_TARGET_LANGUAGE) { + showLanguageOptions(key); + return true; + } else { + return super.onLongPress(key); + } + } +}