From c79fca652a225cae79c00fd02a934be1f0927607 Mon Sep 17 00:00:00 2001 From: "V. Khmelevskiy" Date: Mon, 18 Feb 2019 20:09:19 +0700 Subject: [PATCH] Android basic keyboard input support (incl. virtual keyboard) Normal English input works fine. Punctuation and navigation are partially supported. IME from virtual keyboard doesn't work at all, seems like the native activity wrapper(android_app class) issues after Android 4.0. --- android/android_ldc_armv7a.mk | 13 +- android/dlangui_source_files.mk | 1 + src/dlangui/platforms/android/androidapp.d | 120 ++++++++++- src/dlangui/platforms/android/imm.d | 220 +++++++++++++++++++++ src/dlangui/widgets/editors.d | 4 +- src/dlangui/widgets/widget.d | 19 ++ 6 files changed, 372 insertions(+), 5 deletions(-) create mode 100644 src/dlangui/platforms/android/imm.d diff --git a/android/android_ldc_armv7a.mk b/android/android_ldc_armv7a.mk index 467b010c..26d9b7ca 100644 --- a/android/android_ldc_armv7a.mk +++ b/android/android_ldc_armv7a.mk @@ -2,9 +2,16 @@ PLATFORM_DIR=armeabi-v7a echo "\nLOCAL_MODULE: $LOCAL_MODULE" + +OS_TYPE="unknown" +case "$OSTYPE" in + msys*|cygwin*) OS_TYPE="windows" ;; + linux*) OS_TYPE="linux" ;; +esac + LDC_PARAMS="-mtriple=armv7-none-linux-androideabi -relocation-model=pic " -export LD=$NDK/toolchains/llvm/prebuilt/linux-$NDK_ARCH/bin/llvm-link -export CC=$NDK/toolchains/llvm/prebuilt/linux-$NDK_ARCH/bin/clang +export LD=$NDK/toolchains/llvm/prebuilt/$OS_TYPE-$NDK_ARCH/bin/llvm-link +export CC=$NDK/toolchains/llvm/prebuilt/$OS_TYPE-$NDK_ARCH/bin/clang SOURCES="$LOCAL_SRC_FILES $DLANGUI_SOURCES" SOURCE_PATHS="-I./jni $DLANGUI_SOURCE_PATHS $DLANGUI_IMPORT_PATHS" @@ -24,7 +31,7 @@ LINK_OPTIONS="\ -Wl,-soname,libnative-activity.so \ -shared \ --sysroot=$NDK/platforms/$ANDROID_TARGET/arch-arm \ --gcc-toolchain $NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-$NDK_ARCH \ +-gcc-toolchain $NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/$OS_TYPE-$NDK_ARCH \ -no-canonical-prefixes \ -target armv7-none-linux-androideabi \ -Wl,--fix-cortex-a8 \ diff --git a/android/dlangui_source_files.mk b/android/dlangui_source_files.mk index 2e1b574f..5e772f33 100644 --- a/android/dlangui_source_files.mk +++ b/android/dlangui_source_files.mk @@ -4,6 +4,7 @@ DLANGUI_SOURCES="\ $DLANGUI_DIR/src/dlangui/platforms/android/androidapp.d \ +$DLANGUI_DIR/src/dlangui/platforms/android/imm.d \ $DLANGUI_DIR/src/dlangui/platforms/common/startup.d \ $DLANGUI_DIR/src/dlangui/platforms/common/platform.d \ $DLANGUI_DIR/src/dlangui/dialogs/filedlg.d \ diff --git a/src/dlangui/platforms/android/androidapp.d b/src/dlangui/platforms/android/androidapp.d index f76ef462..13e3fd9e 100644 --- a/src/dlangui/platforms/android/androidapp.d +++ b/src/dlangui/platforms/android/androidapp.d @@ -16,6 +16,7 @@ import dlangui.platforms.common.platform; import android.input, android.looper : ALooper_pollAll; import android.native_window : ANativeWindow_setBuffersGeometry; import android.configuration; +import android.keycodes; import android.log, android.android_native_app_glue; /** @@ -139,6 +140,8 @@ class AndroidWindow : Window { * Process the next input event. */ int handle_input(AInputEvent* event) { + import imm = dlangui.platforms.android.imm; + import std.conv : to; Log.i("handle input, event=", AInputEvent_getType(event)); auto et = AInputEvent_getType(event); if (et == AINPUT_EVENT_TYPE_MOTION) { @@ -173,7 +176,37 @@ class AndroidWindow : Window { return 1; } else if (et == AINPUT_EVENT_TYPE_KEY) { Log.d("AINPUT_EVENT_TYPE_KEY"); - return 0; + KeyEvent evt; + auto app = (cast(AndroidPlatform)platform)._appstate; + int _keyFlags = AKeyEvent_getMetaState(event).toKeyFlag(); + int sysKeyCode = AKeyEvent_getKeyCode(event); + int sysMeta = AKeyEvent_getMetaState(event); + int keyCode = androidKeyMap.get(sysKeyCode, KeyCode.init); + auto action = toKeyAction(AKeyEvent_getAction(event)); + int char_ = imm.GetUnicodeChar(app, action, sysKeyCode, sysMeta); + dchar[] text; + if (!isTextEditControl(sysKeyCode)) { + if (AKeyEvent_getAction(event) == AKEY_EVENT_ACTION_MULTIPLE) { + // it's a string from IME + if (sysKeyCode == AKEYCODE_UNKNOWN) { + text = cast(dchar[]) to!dstring(imm.GetUnicodeString(app, event)); + action = KeyAction.Text; + } + // else repeat character AKeyEvent_getRepeatCount() times + } + else if (AKeyEvent_getAction(event) == AKEY_EVENT_ACTION_DOWN && (char_ || isASCIIChar(sysKeyCode))) { + text ~= cast(dchar)(char_ == 0 ? sysKeyCode : char_); + action = KeyAction.Text; + } + } + Log.d("ACTION: ", action, " syskeyCode: ", sysKeyCode, " sysMeta: ", sysMeta, "meta: ", _keyFlags, " char '", cast(dchar)char_, "' str:", cast(dstring)text); + if (action == KeyAction.Text) + evt = new KeyEvent(KeyAction.Text, 0, 0, cast(dstring)text); + else + evt = new KeyEvent(action, keyCode, _keyFlags); + if (evt && dispatchKeyEvent(evt)) + update(); + return 1; } return 0; } @@ -225,6 +258,12 @@ class AndroidPlatform : Platform { } + void showSoftKeyboard(bool shouldShow) { + import imm = dlangui.platforms.android.imm; + imm.showSoftKeyboard(_appstate, shouldShow); + } + + /** * Initialize an EGL context for the current display. */ @@ -706,3 +745,82 @@ extern (C) void android_main(android_app* state) { } + +private KeyAction toKeyAction(int androidKeyAction) { + switch(androidKeyAction) { + case AKEY_EVENT_ACTION_DOWN: return KeyAction.KeyDown; + case AKEY_EVENT_ACTION_UP: return KeyAction.KeyUp; + case AKEY_EVENT_ACTION_MULTIPLE: return KeyAction.Repeat; // can also be text + default: + assert(0, "should never reach this"); + } +} + + +private bool isASCIIChar(int ch) { + return 31 < ch && ch < 127; +} + +// Text editor controls such as move caret or (de)select +private bool isTextEditControl(int keyCode) { + switch (keyCode){ + case AKEYCODE_DEL: // backspace ("DEL" or "", "(II)V"); + jobject eventObj = (*env).NewObject(env, class_key_event, eventConstructor, eventType, keyCode); + + unicodeKey = (*env).CallIntMethod(env, eventObj, method_get_unicode_char); + } + else + { + jmethodID method_get_unicode_char = (*env).GetMethodID(env, class_key_event, "getUnicodeChar", "(I)I"); + jmethodID eventConstructor = (*env).GetMethodID(env, class_key_event, "", "(II)V"); + jobject eventObj = (*env).NewObject(env, class_key_event, eventConstructor, eventType, keyCode); + + unicodeKey = (*env).CallIntMethod(env, eventObj, method_get_unicode_char, metaState); + } + + (*javaVM).DetachCurrentThread(javaVM); + + return unicodeKey; +} + + +// Issue: native app glue seems to mess up the input. +// It is clearly seen in debugger that initally key event do have real input, +// but second time it is called it is all messed up +string GetUnicodeString(android_app* app, AInputEvent* event) +{ + string str; + auto javaVM = app.activity.vm; + auto env = app.activity.env; + + JavaVMAttachArgs attachArgs; + attachArgs.version_ = JNI_VERSION_1_6; + attachArgs.name = "NativeThread"; + attachArgs.group = null; + + if ((*javaVM).AttachCurrentThread(javaVM, &env, &attachArgs) == JNI_ERR) + { + Log.e("showSoftKeyboard Unable to attach to JVM"); + return null; + } + + + jclass class_key_event = (*env).FindClass(env, "android/view/KeyEvent"); + + jmethodID eventConstructor = (*env).GetMethodID(env, class_key_event, "", "(JJIIIIIIII)V"); + jobject eventObj = (*env).NewObject(env, class_key_event, eventConstructor, + AKeyEvent_getDownTime(event), + AKeyEvent_getEventTime(event), + AKeyEvent_getAction(event), + AKeyEvent_getKeyCode(event), + AKeyEvent_getRepeatCount(event), + AKeyEvent_getMetaState(event), + AInputEvent_getDeviceId(event), + AKeyEvent_getScanCode(event), + AKeyEvent_getFlags(event), + AInputEvent_getSource(event) + ); + + // this won't work because characters is a member passed on construction and getCharacter() is just a getter + jmethodID method_get_characters = (*env).GetMethodID(env, class_key_event, "getCharacters", "()Ljava/lang/String;"); + if (auto jstr = (*env).CallObjectMethod(env, eventObj, method_get_characters)) { + str.length = (*env).GetStringUTFLength(env, jstr); + (*env).GetStringUTFRegion(env, jstr, 0, str.length, cast(char*)str.ptr); + } + + { + jmethodID method_get_unicode_char = (*env).GetMethodID(env, class_key_event, "getUnicodeChar", "()I"); + int unicodeKey = (*env).CallIntMethod(env, eventObj, method_get_unicode_char); + if (str.length == 0) { + import std.conv : to; + dchar[] tmp; + tmp ~= unicodeKey; + str = to!string(tmp); + } + } + + (*javaVM).DetachCurrentThread(javaVM); + + return str; +} \ No newline at end of file diff --git a/src/dlangui/widgets/editors.d b/src/dlangui/widgets/editors.d index 88857657..534b003c 100644 --- a/src/dlangui/widgets/editors.d +++ b/src/dlangui/widgets/editors.d @@ -345,8 +345,10 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction /// sets focus to this widget or suitable focusable child, returns previously focused widget override Widget setFocus(FocusReason reason = FocusReason.Unspecified) { Widget res = super.setFocus(reason); - if (focused) + if (focused) { + showSoftKeyboard(); handleEditorStateChange(); + } return res; } diff --git a/src/dlangui/widgets/widget.d b/src/dlangui/widgets/widget.d index 655d6762..294a5343 100644 --- a/src/dlangui/widgets/widget.d +++ b/src/dlangui/widgets/widget.d @@ -1099,6 +1099,7 @@ public: // try to find focusable child return window.focusedWidget; } + hideSoftKeyboard(); return window.setFocus(this, reason); } /// searches children for first focusable item, returns null if not found @@ -1116,6 +1117,24 @@ public: return null; } + /// + final void hideSoftKeyboard() { + version(Android) { + import dlangui.platforms.android.androidapp; + if (auto androidPlatform = cast(AndroidPlatform)platform) + androidPlatform.showSoftKeyboard(false); + } + } + + /// Shows system virtual keyabord where applicable + final void showSoftKeyboard() { + version(Android) { + import dlangui.platforms.android.androidapp; + if (auto androidPlatform = cast(AndroidPlatform)platform) + androidPlatform.showSoftKeyboard(true); + } + } + // ======================================================= // Events