diff --git a/android/Android.mk b/android/Android.mk index 44f4b77c2..83b27d586 100644 --- a/android/Android.mk +++ b/android/Android.mk @@ -166,7 +166,8 @@ LOCAL_CFLAGS := -I../lib/angelscript/include \ -DNDEBUG \ -DANDROID_PACKAGE_NAME=\"$(PACKAGE_NAME)\" \ -DANDROID_APP_DIR_NAME=\"$(APP_DIR_NAME)\" \ - -DSUPERTUXKART_VERSION=\"$(PROJECT_VERSION)\" + -DSUPERTUXKART_VERSION=\"$(PROJECT_VERSION)\" \ + -DANDROID_PACKAGE_CALLBACK_NAME=$(PACKAGE_CALLBACK_NAME) LOCAL_CPPFLAGS := -std=gnu++0x LOCAL_STATIC_LIBRARIES := irrlicht bullet enet freetype ifaddrs angelscript \ diff --git a/android/make.sh b/android/make.sh index d34f90c24..6fab52cc6 100755 --- a/android/make.sh +++ b/android/make.sh @@ -471,7 +471,22 @@ sed -i "s/targetSdkVersion=\".*\"/targetSdkVersion=\"$TARGET_SDK_VERSION\"/g" \ sed -i "s/package=\".*\"/package=\"$PACKAGE_NAME\"/g" \ "$DIRNAME/AndroidManifest.xml" -sed -i "s/package .*/package $PACKAGE_NAME;/g" \ +sed -i "s/package org.supertuxkart.*/package $PACKAGE_NAME;/g" \ + "$DIRNAME/src/main/java/STKEditText.java" + +sed -i "s/import org.supertuxkart.*/import $PACKAGE_NAME.STKInputConnection;/g" \ + "$DIRNAME/src/main/java/STKEditText.java" + +sed -i "s/package org.supertuxkart.*/package $PACKAGE_NAME;/g" \ + "$DIRNAME/src/main/java/STKInputConnection.java" + +sed -i "s/import org.supertuxkart.*.STKEditText;/import $PACKAGE_NAME.STKEditText;/g" \ + "$DIRNAME/src/main/java/STKInputConnection.java" + +sed -i "s/package org.supertuxkart.*/package $PACKAGE_NAME;/g" \ + "$DIRNAME/src/main/java/SuperTuxKartActivity.java" + +sed -i "s/import org.supertuxkart.*/import $PACKAGE_NAME.STKEditText;/g" \ "$DIRNAME/src/main/java/SuperTuxKartActivity.java" sed -i "s/versionName=\".*\"/versionName=\"$PROJECT_VERSION\"/g" \ @@ -479,7 +494,7 @@ sed -i "s/versionName=\".*\"/versionName=\"$PROJECT_VERSION\"/g" \ sed -i "s/versionCode=\".*\"/versionCode=\"$PROJECT_CODE\"/g" \ "$DIRNAME/AndroidManifest.xml" - + cp "banner.png" "$DIRNAME/res/drawable/banner.png" cp "$APP_ICON" "$DIRNAME/res/drawable/icon.png" convert -scale 72x72 "$APP_ICON" "$DIRNAME/res/drawable-hdpi/icon.png" diff --git a/android/src/main/java/STKEditText.java b/android/src/main/java/STKEditText.java new file mode 100644 index 000000000..50277031d --- /dev/null +++ b/android/src/main/java/STKEditText.java @@ -0,0 +1,78 @@ +package org.supertuxkart.stk_dbg; + +import org.supertuxkart.stk_dbg.STKInputConnection; + +import android.content.Context; +import android.text.InputType; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.widget.EditText; + +// We need to extend EditText instead of view to allow copying to our STK +// editbox +public class STKEditText extends EditText +{ + private int m_composing_start; + + private int m_composing_end; + + // ------------------------------------------------------------------------ + private native static void editText2STKEditbox(String full_text, int start, + int end, int composing_start, + int composing_end); + // ------------------------------------------------------------------------ + public STKEditText(Context context) + { + super(context); + setFocusableInTouchMode(true); + m_composing_start = 0; + m_composing_end = 0; + } + // ------------------------------------------------------------------------ + @Override + public InputConnection onCreateInputConnection(EditorInfo out_attrs) + { + STKInputConnection sic = + new STKInputConnection(super.onCreateInputConnection(out_attrs), this); + out_attrs.actionLabel = null; + out_attrs.inputType = InputType.TYPE_CLASS_TEXT; + out_attrs.imeOptions = EditorInfo.IME_ACTION_NEXT | + EditorInfo.IME_FLAG_NO_FULLSCREEN | + EditorInfo.IME_FLAG_NO_EXTRACT_UI; + return sic; + } + // ------------------------------------------------------------------------ + @Override + public boolean onCheckIsTextEditor() { return true; } + // ------------------------------------------------------------------------ + public void resetWhenFocus() + { + clearComposingText(); + getText().clear(); + m_composing_start = m_composing_end = 0; + } + // ------------------------------------------------------------------------ + public void setComposingRegion(int start, int end) + { + // From doc of InputConnectionWrapper, it says: + // Editor authors, be ready to accept a start that is greater than end. + if (start != end && start > end) + { + m_composing_end = start; + m_composing_start = end; + } + else + { + m_composing_start = start; + m_composing_end = end; + } + } + // ------------------------------------------------------------------------ + public void updateSTKEditBox() + { + if (!isFocused()) + return; + editText2STKEditbox(getText().toString(), getSelectionStart(), + getSelectionEnd(), m_composing_start, m_composing_end); + } +} diff --git a/android/src/main/java/STKInputConnection.java b/android/src/main/java/STKInputConnection.java new file mode 100644 index 000000000..a53c93d75 --- /dev/null +++ b/android/src/main/java/STKInputConnection.java @@ -0,0 +1,68 @@ +package org.supertuxkart.stk_dbg; + +import org.supertuxkart.stk_dbg.STKEditText; + +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputConnectionWrapper; + +public class STKInputConnection extends InputConnectionWrapper +{ + /* The global edittext which will be "copied" to the current focused STK + * box. */ + private STKEditText m_stk_edittext; + + // ------------------------------------------------------------------------ + public STKInputConnection(InputConnection target, STKEditText stk_edittext) + { + super(target, true/*mutable*/); + m_stk_edittext = stk_edittext; + } + // ------------------------------------------------------------------------ + @Override + public boolean setComposingText(CharSequence text, int new_cursor_position) + { + boolean ret = super.setComposingText(text, new_cursor_position); + String composing_text = text.toString(); + String new_text = m_stk_edittext.getText().toString(); + int composing_start = 0; + int composing_end = 0; + // Test last char + if (!composing_text.isEmpty() && !new_text.isEmpty() && + composing_text.charAt(composing_text.length() - 1) == + new_text.charAt(new_text.length() - 1)) + { + composing_start = new_text.length() - composing_text.length(); + composing_end = composing_start + composing_text.length(); + } + m_stk_edittext.setComposingRegion(composing_start, composing_end); + m_stk_edittext.updateSTKEditBox(); + return ret; + } + // ------------------------------------------------------------------------ + @Override + public boolean finishComposingText() + { + m_stk_edittext.setComposingRegion(0, 0); + m_stk_edittext.updateSTKEditBox(); + return super.finishComposingText(); + } + // ------------------------------------------------------------------------ + @Override + public boolean setComposingRegion(int start, int end) + { + m_stk_edittext.setComposingRegion(start, end); + m_stk_edittext.updateSTKEditBox(); + return super.setComposingRegion(start, end); + } + // ------------------------------------------------------------------------ + @Override + public boolean commitText(CharSequence text, int new_cursor_position) + { + // Usually only a single character, so dismiss composing region + boolean ret = super.commitText(text, new_cursor_position); + m_stk_edittext.setComposingRegion(0, 0); + m_stk_edittext.updateSTKEditBox(); + return ret; + } + +} diff --git a/android/src/main/java/SuperTuxKartActivity.java b/android/src/main/java/SuperTuxKartActivity.java index 7561da0b7..94b9925d1 100644 --- a/android/src/main/java/SuperTuxKartActivity.java +++ b/android/src/main/java/SuperTuxKartActivity.java @@ -1,20 +1,27 @@ package org.supertuxkart.stk_dbg; +import org.supertuxkart.stk_dbg.STKEditText; + import android.app.NativeActivity; import android.content.Context; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; import android.view.inputmethod.InputMethodManager; import android.view.KeyEvent; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.View; +import android.widget.FrameLayout; public class SuperTuxKartActivity extends NativeActivity { - private native void saveFromJavaChars(String chars); - private native void saveKeyboardHeight(int height); + private STKEditText m_stk_edittext; + // ------------------------------------------------------------------------ + private native void saveKeyboardHeight(int height); + // ------------------------------------------------------------------------ private void hideNavBar(View decor_view) { if (Build.VERSION.SDK_INT < 19) @@ -27,12 +34,14 @@ public class SuperTuxKartActivity extends NativeActivity View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); } - + // ------------------------------------------------------------------------ @Override public void onCreate(Bundle instance) { super.onCreate(instance); System.loadLibrary("main"); + m_stk_edittext = null; + final View root = getWindow().getDecorView().findViewById( android.R.id.content); root.getViewTreeObserver().addOnGlobalLayoutListener(new @@ -61,26 +70,7 @@ public class SuperTuxKartActivity extends NativeActivity } }); } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) - { - // ACTION_MULTIPLE deprecated in API level Q, it says if the key code - // is KEYCODE_UNKNOWN, then this is a sequence of characters as - // returned by getCharacters() - if (event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN && - event.getAction() == KeyEvent.ACTION_MULTIPLE) - { - String chars = event.getCharacters(); - if (chars != null) - { - saveFromJavaChars(chars); - return true; - } - } - return super.dispatchKeyEvent(event); - } - + // ------------------------------------------------------------------------ @Override public void onWindowFocusChanged(boolean has_focus) { @@ -88,20 +78,88 @@ public class SuperTuxKartActivity extends NativeActivity if (has_focus) hideNavBar(getWindow().getDecorView()); } - + // ------------------------------------------------------------------------ + @Override + public boolean dispatchKeyEvent(KeyEvent event) + { + // Called when user change cursor / select all text in native android + // keyboard + boolean ret = super.dispatchKeyEvent(event); + if (m_stk_edittext != null) + m_stk_edittext.updateSTKEditBox(); + return ret; + } + // ------------------------------------------------------------------------ public void showKeyboard() { - InputMethodManager imm = (InputMethodManager) - getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(getWindow().getDecorView(), - InputMethodManager.SHOW_FORCED); - } + final Context context = this; + // Need to run in ui thread as it access the view m_stk_edittext + runOnUiThread(new Runnable() + { + @Override + public void run() + { + InputMethodManager imm = (InputMethodManager) + getSystemService(Context.INPUT_METHOD_SERVICE); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT); + if (m_stk_edittext == null) + { + m_stk_edittext = new STKEditText(context); + // For some copy-and-paste text are not done by commitText + // in STKInputConnection, so we need an extra watcher + m_stk_edittext.addTextChangedListener(new TextWatcher() + { + @Override + public void onTextChanged(CharSequence s, + int start, int before, + int count) {} + @Override + public void beforeTextChanged(CharSequence s, + int start, int count, + int after) {} + @Override + public void afterTextChanged(Editable edit) + { + if (m_stk_edittext != null) + m_stk_edittext.updateSTKEditBox(); + } + }); + addContentView(m_stk_edittext, params); + } + else + m_stk_edittext.setLayoutParams(params); + + m_stk_edittext.resetWhenFocus(); + m_stk_edittext.setVisibility(View.VISIBLE); + m_stk_edittext.requestFocus(); + + imm.showSoftInput(m_stk_edittext, + InputMethodManager.SHOW_FORCED); + } + }); + } + // ------------------------------------------------------------------------ public void hideKeyboard() { - InputMethodManager imm = (InputMethodManager) - getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow( - getWindow().getDecorView().getWindowToken(), 0); + runOnUiThread(new Runnable() + { + @Override + public void run() + { + if (m_stk_edittext == null) + return; + + m_stk_edittext.clearFocus(); + m_stk_edittext.setVisibility(View.GONE); + + InputMethodManager imm = (InputMethodManager) + getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(m_stk_edittext.getWindowToken(), + 0); + } + }); } } diff --git a/src/guiengine/widgets/CGUIEditBox.cpp b/src/guiengine/widgets/CGUIEditBox.cpp index 8818c005e..207ac0f43 100644 --- a/src/guiengine/widgets/CGUIEditBox.cpp +++ b/src/guiengine/widgets/CGUIEditBox.cpp @@ -23,6 +23,8 @@ #include "../../../lib/irrlicht/include/IrrCompileConfig.h" #include "../../../lib/irrlicht/source/Irrlicht/CIrrDeviceLinux.h" +#include + #ifdef ANDROID #include "../../../lib/irrlicht/source/Irrlicht/CIrrDeviceAndroid.h" #endif @@ -69,6 +71,9 @@ CGUIEditBox::CGUIEditBox(const wchar_t* text, bool border, // FIXME quick hack to enable mark movement with keyboard and mouse for rtl language, // don't know why it's disabled in the first place, because STK fail // to input unicode characters before? + m_from_android_edittext = false; + m_composing_start = 0; + m_composing_end = 0; #ifdef _DEBUG setDebugName("CGUIEditBox"); @@ -261,6 +266,9 @@ bool CGUIEditBox::OnEvent(const SEvent& event) #ifndef SERVER_ONLY if (isEnabled()) { + // Ignore key input if we only fromAndroidEditText + if (m_from_android_edittext && event.EventType == EET_KEY_INPUT_EVENT) + return true; switch(event.EventType) { case EET_GUI_EVENT: @@ -287,6 +295,9 @@ bool CGUIEditBox::OnEvent(const SEvent& event) dl->setTextInputEnabled(false); } #endif + m_from_android_edittext = false; + m_composing_start = 0; + m_composing_end = 0; } else if (event.GUIEvent.EventType == EGET_ELEMENT_FOCUSED) { @@ -1073,6 +1084,19 @@ void CGUIEditBox::draw() // it will return the input pointer if (this->isRTLLanguage()) from Translations::isRTLText // is false + // draw composing text underline + if (!PasswordBox && focus && m_composing_start != m_composing_end && i == hlineStart) + { + s = txtLine->subString(0, m_composing_start); + s32 underline_begin = font->getDimension(s.c_str()).Width; + core::rect underline = CurrentTextRect; + underline.UpperLeftCorner.X += underline_begin; + s32 height = underline.LowerRightCorner.Y - underline.UpperLeftCorner.Y; + underline.UpperLeftCorner.Y += s32(std::abs(height) * 0.9f); + underline.LowerRightCorner.Y -= s32(std::abs(height) * 0.08f); + GL32_draw2DRectangle(video::SColor(255, 0, 0, 0), underline); + } + // draw mark and marked text if (focus && MarkBegin != MarkEnd && i >= hlineStart && i < hlineStart + hlineCount) { @@ -1780,3 +1804,40 @@ void CGUIEditBox::openScreenKeyboard() new GUIEngine::ScreenKeyboard(1.0f, 0.40f, this); } +// Real copying is happening in text_box_widget.cpp with static function +void CGUIEditBox::fromAndroidEditText(const core::stringw& text, int start, + int end, int composing_start, + int composing_end) +{ + // When focus of this element is lost, this will be set to false again + m_from_android_edittext = true; + Text = text; + // Prevent invalid start or end + if ((unsigned)end > Text.size()) + { + end = (int)Text.size(); + start = end; + } + + CursorPos = end; + m_composing_start = 0; + m_composing_end = 0; + + if (start != end) + setTextMarkers(start, end); + else + { + MarkBegin = 0; + MarkEnd = 0; + } + + if (composing_start != composing_end) + { + if (composing_start < 0) + composing_start = 0; + if (composing_end > end) + composing_end = end; + m_composing_start = composing_start; + m_composing_end = composing_end; + } +} diff --git a/src/guiengine/widgets/CGUIEditBox.hpp b/src/guiengine/widgets/CGUIEditBox.hpp index cf3a0dd56..d7a4a5c4b 100644 --- a/src/guiengine/widgets/CGUIEditBox.hpp +++ b/src/guiengine/widgets/CGUIEditBox.hpp @@ -118,7 +118,9 @@ using namespace gui; virtual irr::gui::IGUIFont* getOverrideFont() const { return NULL; } virtual irr::gui::IGUIFont* getActiveFont() const { return NULL; } virtual void setDrawBackground(bool) { } - + + void fromAndroidEditText(const core::stringw& text, int start, int end, + int composing_start, int composing_end); void openScreenKeyboard(); s32 getCursorPosInBox() const { return CursorPos; } s32 getTextCount() const { return (s32)Text.size(); } @@ -174,6 +176,13 @@ using namespace gui; core::array< s32 > BrokenTextPositions; core::rect CurrentTextRect, FrameRect; // temporary values + + s32 m_composing_start; + s32 m_composing_end; + + /* If true, this editbox will copy text and selection only from + * android edittext, and process only mouse event. */ + bool m_from_android_edittext; }; diff --git a/src/guiengine/widgets/text_box_widget.cpp b/src/guiengine/widgets/text_box_widget.cpp index e6d7712a9..f9066f868 100644 --- a/src/guiengine/widgets/text_box_widget.cpp +++ b/src/guiengine/widgets/text_box_widget.cpp @@ -228,3 +228,34 @@ EventPropagation TextBoxWidget::leftPressed (const int playerID) return EVENT_LET; } // leftPressed + +// ============================================================================ +/* This callback will allow copying android edittext data directly to editbox, + * which will allow composing text to be auto updated. */ +#ifdef ANDROID +#include "jni.h" + +#if !defined(ANDROID_PACKAGE_CALLBACK_NAME) + #error +#endif + +#define MAKE_EDITTEXT_CALLBACK(x) JNIEXPORT void JNICALL Java_ ## x##_STKEditText_editText2STKEditbox(JNIEnv* env, jobject this_obj, jstring text, jint start, jint end, jint composing_start, jint composing_end) +#define ANDROID_EDITTEXT_CALLBACK(PKG_NAME) MAKE_EDITTEXT_CALLBACK(PKG_NAME) + +extern "C" +ANDROID_EDITTEXT_CALLBACK(ANDROID_PACKAGE_CALLBACK_NAME) +{ + TextBoxWidget* tb = dynamic_cast(getFocusForPlayer(0)); + if (!tb || text == NULL) + return; + + const char* utf8_text = env->GetStringUTFChars(text, NULL); + if (utf8_text == NULL) + return; + + core::stringw to_editbox = StringUtils::utf8ToWide(utf8_text); + tb->getIrrlichtElement()->fromAndroidEditText( + to_editbox, start, end, composing_start, composing_end); + env->ReleaseStringUTFChars(text, utf8_text); +} +#endif