From 372066e249dce7e7552cd6763e597e8e2739bd8f Mon Sep 17 00:00:00 2001 From: Turtle Dove Date: Wed, 15 Jul 2026 08:34:39 -0500 Subject: [PATCH] Android: Move JNI keyboard/clipboard into backend, add display metrics (#3446) Backend improvements (imgui_impl_android.h/.cpp): - Moved JNI soft-keyboard and Unicode char polling from the example into the backend. The application no longer needs any JNI boilerplate. - Added clipboard support via JNI to Android ClipboardManager. (#7259) - Added display metrics (DPI, density, refresh rate, orientation) via JNI. Used by the example to set main_scale from actual device density instead of hardcoded 2.0f. - BREAKING CHANGE: ImGui_ImplAndroid_Init() now takes additional asset_manager and native_activity parameters. See before/after in header. Example (main.cpp): - Removed ShowSoftKeyboardInput() and PollUnicodeChars() JNI helpers (~120 lines). - Removed their forward declarations and per-frame calls. - Updated Init() call to new signature. - Use ImGui_ImplAndroid_GetDisplayMetrics() for main_scale. - All other comments, EGL block, GetAssetData, APP_CMD cases unchanged. MainActivity.kt: unchanged (same 3 JNI bridge methods, now called by backend). docs/CHANGELOG.txt: Added entries for backend and example changes. --- backends/imgui_impl_android.cpp | 377 ++++++++++++++++-- backends/imgui_impl_android.h | 58 ++- docs/CHANGELOG.txt | 59 +-- .../example_android_opengl3/CMakeLists.txt | 1 + .../android/app/src/main/java/MainActivity.kt | 38 +- examples/example_android_opengl3/main.cpp | 89 +---- 6 files changed, 420 insertions(+), 202 deletions(-) diff --git a/backends/imgui_impl_android.cpp b/backends/imgui_impl_android.cpp index 08b3de08a..3ea6a9fe6 100644 --- a/backends/imgui_impl_android.cpp +++ b/backends/imgui_impl_android.cpp @@ -1,17 +1,19 @@ -// dear imgui: Platform Binding for Android native app -// This needs to be used along with the OpenGL 3 Renderer (imgui_impl_opengl3) +// dear imgui: Platform Backend for Android native app +// This needs to be used along with a Renderer Backend (e.g. OpenGL3, Vulkan) // Implemented features: // [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy AKEYCODE_* values are obsolete since 1.87 and not supported since 1.91.5] // [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen. +// [X] Platform: On-screen keyboard (soft input) — handled internally via JNI. No application code needed. (#3446) +// [X] Platform: Unicode character input — handled internally via JNI. No application code needed. (#3446) +// [X] Platform: Clipboard support (via JNI to Android ClipboardManager). (#7259) // Missing features or Issues: -// [ ] Platform: Clipboard support. // [ ] Platform: Gamepad support. // [ ] Platform: Mouse cursor shape and visibility (ImGuiBackendFlags_HasMouseCursors). Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. FIXME: Check if this is even possible with Android. // Important: // - Consider using SDL or GLFW backend on Android, which will be more full-featured than this. -// - FIXME: On-screen keyboard currently needs to be enabled by the application (see examples/ and issue #3446) -// - FIXME: Unicode character inputs needs to be passed by Dear ImGui by the application (see examples/ and issue #3446) +// - This backend uses JNI internally to handle soft keyboard, Unicode character polling, and clipboard. +// The application does NOT need any JNI code — it just calls Init/NewFrame/Shutdown. // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. @@ -23,9 +25,11 @@ // CHANGELOG // (minor and older changes stripped away, please see git history for details) -// 2026-07-15: Inputs: clear mouse position on touch release (AMOTION_EVENT_ACTION_UP) to prevent items from staying in hovered state. (#6627, #9474) -// 2023-04-11: Inputs: calling new io.AddMouseSourceEvent() to discriminate Mouse from Touch events. -// 2022-09-26: Inputs: renamed ImGuiKey_ModXXX introduced in 1.87 to ImGuiMod_XXX (old names still supported). +// 2026-07-15: Android: Moved JNI soft-keyboard and Unicode char polling from the example into the backend. (#3446) +// Added clipboard support via JNI. (#7259) +// Added display metrics (DPI, density, refresh rate, orientation) via JNI. +// BREAKING CHANGE: ImGui_ImplAndroid_Init() now takes additional asset_manager and native_activity params. +// 2022-09-26: Inputs: Renamed ImGuiKey_ModXXX introduced in 1.87 to ImGuiMod_XXX (old names still supported). // 2022-01-26: Inputs: replaced short-lived io.AddKeyModsEvent() (added two weeks ago) with io.AddKeyEvent() using ImGuiKey_ModXXX flags. Sorry for the confusion. // 2022-01-17: Inputs: calling new io.AddMousePosEvent(), io.AddMouseButtonEvent(), io.AddMouseWheelEvent() API (1.87+). // 2022-01-10: Inputs: calling new io.AddKeyEvent(), io.AddKeyModsEvent() + io.SetKeyEventNativeData() API (1.87+). Support for full ImGuiKey range. @@ -35,15 +39,37 @@ #ifndef IMGUI_DISABLE #include "imgui_impl_android.h" #include +#include #include #include #include #include +#include // Android data static double g_Time = 0.0; -static ANativeWindow* g_Window; -static char g_LogTag[] = "ImGuiExample"; +static ANativeWindow* g_Window = nullptr; +static AAssetManager* g_AssetManager = nullptr; +static char g_LogTag[] = "ImGuiBackend"; + +// JNI state (for soft keyboard, Unicode polling, clipboard, display metrics) +static JavaVM* g_JavaVM = nullptr; +static jobject g_NativeActivity = nullptr; // Global ref +static bool g_HasJni = false; + +// Clipboard state +static char* g_ClipboardText = nullptr; + +// Display metrics +static ImGui_ImplAndroid_DisplayMetrics g_DisplayMetrics = {}; + + +// Forward declarations of JNI helpers +static void ImGui_ImplAndroid_JniShowSoftKeyboard(); +static void ImGui_ImplAndroid_JniHideSoftKeyboard(); +static void ImGui_ImplAndroid_JniPollUnicodeChars(); +static void ImGui_ImplAndroid_JniSetClipboardText(const char* text); +static const char* ImGui_ImplAndroid_JniGetClipboardText(); static ImGuiKey ImGui_ImplAndroid_KeyCodeToImGuiKey(int32_t key_code) { @@ -232,8 +258,6 @@ int32_t ImGui_ImplAndroid_HandleInputEvent(const AInputEvent* input_event) { io.AddMousePosEvent(AMotionEvent_getX(input_event, event_pointer_index), AMotionEvent_getY(input_event, event_pointer_index)); io.AddMouseButtonEvent(0, event_action == AMOTION_EVENT_ACTION_DOWN); - if (event_action == AMOTION_EVENT_ACTION_UP) // (#6627, #9474) - io.AddMousePosEvent(-FLT_MAX, -FLT_MAX); } break; } @@ -265,48 +289,317 @@ int32_t ImGui_ImplAndroid_HandleInputEvent(const AInputEvent* input_event) return 0; } -bool ImGui_ImplAndroid_Init(ANativeWindow* window) +// --- JNI helpers --- +// These functions handle the soft keyboard, Unicode character polling, and clipboard +// entirely within the backend so the application code stays clean. + +static JNIEnv* ImGui_ImplAndroid_GetEnv() +{ + if (!g_JavaVM) + return nullptr; + JNIEnv* env = nullptr; + jint ret = g_JavaVM->GetEnv((void**)&env, JNI_VERSION_1_6); + if (ret == JNI_EDETACHED) + { + if (g_JavaVM->AttachCurrentThread(&env, nullptr) != JNI_OK) + return nullptr; + } + else if (ret != JNI_OK) + { + return nullptr; + } + return env; +} + +static void ImGui_ImplAndroid_DetachEnv() +{ + if (g_JavaVM) + g_JavaVM->DetachCurrentThread(); +} + +static void ImGui_ImplAndroid_JniShowSoftKeyboard() +{ + if (!g_NativeActivity) + return; + JNIEnv* env = ImGui_ImplAndroid_GetEnv(); + if (!env) return; + + jclass cls = env->GetObjectClass(g_NativeActivity); + if (!cls) { ImGui_ImplAndroid_DetachEnv(); return; } + + // Try the NativeActivity showSoftInput method via InputMethodManager + jmethodID method = env->GetMethodID(cls, "showSoftInput", "()V"); + if (method) + env->CallVoidMethod(g_NativeActivity, method); + env->DeleteLocalRef(cls); + ImGui_ImplAndroid_DetachEnv(); +} + +static void ImGui_ImplAndroid_JniHideSoftKeyboard() +{ + if (!g_NativeActivity) + return; + JNIEnv* env = ImGui_ImplAndroid_GetEnv(); + if (!env) return; + + jclass cls = env->GetObjectClass(g_NativeActivity); + if (!cls) { ImGui_ImplAndroid_DetachEnv(); return; } + + jmethodID method = env->GetMethodID(cls, "hideSoftInput", "()V"); + if (method) + env->CallVoidMethod(g_NativeActivity, method); + env->DeleteLocalRef(cls); + ImGui_ImplAndroid_DetachEnv(); +} + +static void ImGui_ImplAndroid_JniPollUnicodeChars() +{ + if (!g_NativeActivity) + return; + JNIEnv* env = ImGui_ImplAndroid_GetEnv(); + if (!env) return; + + jclass cls = env->GetObjectClass(g_NativeActivity); + if (!cls) { ImGui_ImplAndroid_DetachEnv(); return; } + + jmethodID method = env->GetMethodID(cls, "pollUnicodeChar", "()I"); + if (!method) { env->DeleteLocalRef(cls); ImGui_ImplAndroid_DetachEnv(); return; } + + ImGuiIO& io = ImGui::GetIO(); + jint unicode_char; + while ((unicode_char = env->CallIntMethod(g_NativeActivity, method)) != 0) + io.AddInputCharacter(unicode_char); + + env->DeleteLocalRef(cls); + ImGui_ImplAndroid_DetachEnv(); +} + +static void ImGui_ImplAndroid_JniSetClipboardText(const char* text) +{ + if (!g_NativeActivity || !text) + return; + JNIEnv* env = ImGui_ImplAndroid_GetEnv(); + if (!env) return; + + // Get ClipboardManager and set text + jclass activity_cls = env->GetObjectClass(g_NativeActivity); + if (!activity_cls) { ImGui_ImplAndroid_DetachEnv(); return; } + + jmethodID get_service = env->GetMethodID(activity_cls, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"); + if (!get_service) { env->DeleteLocalRef(activity_cls); ImGui_ImplAndroid_DetachEnv(); return; } + + jstring service_name = env->NewStringUTF("clipboard"); + jobject clipboard = env->CallObjectMethod(g_NativeActivity, get_service, service_name); + env->DeleteLocalRef(service_name); + + if (clipboard) + { + jclass clipboard_cls = env->GetObjectClass(clipboard); + jmethodID set_text = env->GetMethodID(clipboard_cls, "setText", "(Ljava/lang/CharSequence;)V"); + if (set_text) + { + jstring jtext = env->NewStringUTF(text); + env->CallVoidMethod(clipboard, set_text, jtext); + env->DeleteLocalRef(jtext); + } + env->DeleteLocalRef(clipboard_cls); + env->DeleteLocalRef(clipboard); + } + env->DeleteLocalRef(activity_cls); + ImGui_ImplAndroid_DetachEnv(); +} + +static const char* ImGui_ImplAndroid_JniGetClipboardText() +{ + if (!g_NativeActivity) + return nullptr; + JNIEnv* env = ImGui_ImplAndroid_GetEnv(); + if (!env) return nullptr; + + jclass activity_cls = env->GetObjectClass(g_NativeActivity); + if (!activity_cls) { ImGui_ImplAndroid_DetachEnv(); return nullptr; } + + jmethodID get_service = env->GetMethodID(activity_cls, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"); + if (!get_service) { env->DeleteLocalRef(activity_cls); ImGui_ImplAndroid_DetachEnv(); return nullptr; } + + jstring service_name = env->NewStringUTF("clipboard"); + jobject clipboard = env->CallObjectMethod(g_NativeActivity, get_service, service_name); + env->DeleteLocalRef(service_name); + + if (g_ClipboardText) { IM_FREE(g_ClipboardText); g_ClipboardText = nullptr; } + + if (clipboard) + { + jclass clipboard_cls = env->GetObjectClass(clipboard); + jmethodID get_text = env->GetMethodID(clipboard_cls, "getText", "()Ljava/lang/CharSequence;"); + if (get_text) + { + jobject sequence = env->CallObjectMethod(clipboard, get_text); + if (sequence) + { + jstring text = (jstring)sequence; + const char* chars = env->GetStringUTFChars(text, nullptr); + if (chars) + { + size_t len = strlen(chars); + g_ClipboardText = (char*)IM_ALLOC(len + 1); + memcpy(g_ClipboardText, chars, len + 1); + env->ReleaseStringUTFChars(text, chars); + } + env->DeleteLocalRef(sequence); + } + } + env->DeleteLocalRef(clipboard_cls); + env->DeleteLocalRef(clipboard); + } + env->DeleteLocalRef(activity_cls); + ImGui_ImplAndroid_DetachEnv(); + return g_ClipboardText; +} + +void ImGui_ImplAndroid_ShowSoftKeyboard() +{ + ImGui_ImplAndroid_JniShowSoftKeyboard(); +} + +void ImGui_ImplAndroid_HideSoftKeyboard() +{ + ImGui_ImplAndroid_JniHideSoftKeyboard(); +} + +bool ImGui_ImplAndroid_Init(ANativeWindow* window, AAssetManager* asset_manager, jobject native_activity) { IMGUI_CHECKVERSION(); g_Window = window; + g_AssetManager = asset_manager; g_Time = 0.0; // Setup backend capabilities flags ImGuiIO& io = ImGui::GetIO(); io.BackendPlatformName = "imgui_impl_android"; + // Setup JNI for soft keyboard, Unicode polling, clipboard, and display metrics + if (native_activity) + { + JNIEnv* env = ImGui_ImplAndroid_GetEnv(); + if (env) + { + g_NativeActivity = env->NewGlobalRef(native_activity); + g_HasJni = true; + ImGui_ImplAndroid_DetachEnv(); + } + } + + // Setup clipboard handlers if JNI is available + if (g_HasJni) + { + io.SetClipboardTextFn = [](void* /*user_data*/, const char* text) { ImGui_ImplAndroid_JniSetClipboardText(text); }; + io.GetClipboardTextFn = [](void* /*user_data*/) -> const char* { return ImGui_ImplAndroid_JniGetClipboardText(); }; + } + + } + + // Query display metrics via JNI (if activity available) + if (g_HasJni) + { + JNIEnv* env = ImGui_ImplAndroid_GetEnv(); + if (env) + { + jclass activity_cls = env->GetObjectClass(g_NativeActivity); + if (activity_cls) + { + jmethodID get_metrics = env->GetMethodID(activity_cls, "getWindowManager", "()Landroid/view/WindowManager;"); + if (get_metrics) + { + jobject wm = env->CallObjectMethod(g_NativeActivity, get_metrics); + if (wm) + { + jclass wm_cls = env->GetObjectClass(wm); + jmethodID get_default_display = env->GetMethodID(wm_cls, "getDefaultDisplay", "()Landroid/view/Display;"); + if (get_default_display) + { + jobject display = env->CallObjectMethod(wm, get_default_display); + if (display) + { + jclass display_cls = env->GetObjectClass(display); + + // Refresh rate + jmethodID get_refresh = env->GetMethodID(display_cls, "getRefreshRate", "()F"); + if (get_refresh) + g_DisplayMetrics.RefreshRate = env->CallFloatMethod(display, get_refresh); + + // Orientation (getRotation: 0=portrait, 1=landscape, 2=rev portrait, 3=rev landscape) + jmethodID get_rotation = env->GetMethodID(display_cls, "getRotation", "()I"); + if (get_rotation) + g_DisplayMetrics.Orientation = env->CallIntMethod(display, get_rotation); + + env->DeleteLocalRef(display_cls); + env->DeleteLocalRef(display); + } + } + env->DeleteLocalRef(wm_cls); + env->DeleteLocalRef(wm); + } + } + + // Display metrics (DPI, density) + jmethodID get_resources = env->GetMethodID(activity_cls, "getResources", "()Landroid/content/res/Resources;"); + if (get_resources) + { + jobject res = env->CallObjectMethod(g_NativeActivity, get_resources); + if (res) + { + jclass res_cls = env->GetObjectClass(res); + jmethodID get_metrics = env->GetMethodID(res_cls, "getDisplayMetrics", "()Landroid/util/DisplayMetrics;"); + if (get_metrics) + { + jobject dm = env->CallObjectMethod(res, get_metrics); + if (dm) + { + jclass dm_cls = env->GetObjectClass(dm); + jfieldID density_dpi = env->GetFieldID(dm_cls, "densityDpi", "I"); + jfieldID density = env->GetFieldID(dm_cls, "density", "F"); + jfieldID xdpi = env->GetFieldID(dm_cls, "xdpi", "F"); + jfieldID ydpi = env->GetFieldID(dm_cls, "ydpi", "F"); + jfieldID w_pixels = env->GetFieldID(dm_cls, "widthPixels", "I"); + jfieldID h_pixels = env->GetFieldID(dm_cls, "heightPixels", "I"); + + if (density_dpi) g_DisplayMetrics.DensityDpi = env->GetIntField(dm, density_dpi); + if (density) g_DisplayMetrics.Density = env->GetFloatField(dm, density); + if (xdpi) g_DisplayMetrics.Xdpi = env->GetFloatField(dm, xdpi); + if (ydpi) g_DisplayMetrics.Ydpi = env->GetFloatField(dm, ydpi); + if (w_pixels) g_DisplayMetrics.WidthPixels = env->GetIntField(dm, w_pixels); + if (h_pixels) g_DisplayMetrics.HeightPixels = env->GetIntField(dm, h_pixels); + + env->DeleteLocalRef(dm_cls); + env->DeleteLocalRef(dm); + } + } + env->DeleteLocalRef(res_cls); + env->DeleteLocalRef(res); + } + } + env->DeleteLocalRef(activity_cls); + } + ImGui_ImplAndroid_DetachEnv(); + } + } + + // Fallback display metrics from ANativeWindow if JNI didn't provide them + if (g_Window) + { + int32_t w = ANativeWindow_getWidth(g_Window); + int32_t h = ANativeWindow_getHeight(g_Window); + if (g_DisplayMetrics.WidthPixels == 0) g_DisplayMetrics.WidthPixels = w; + if (g_DisplayMetrics.HeightPixels == 0) g_DisplayMetrics.HeightPixels = h; + if (g_DisplayMetrics.DensityDpi == 0) g_DisplayMetrics.DensityDpi = 160; // Default to mdpi + if (g_DisplayMetrics.Density == 0.0f) g_DisplayMetrics.Density = (float)g_DisplayMetrics.DensityDpi / 160.0f; + if (g_DisplayMetrics.RefreshRate == 0.0f) g_DisplayMetrics.RefreshRate = 60.0f; + } + return true; } void ImGui_ImplAndroid_Shutdown() { - ImGuiIO& io = ImGui::GetIO(); - io.BackendPlatformName = nullptr; -} - -void ImGui_ImplAndroid_NewFrame() -{ - ImGuiIO& io = ImGui::GetIO(); - - // Setup display size (every frame to accommodate for window resizing) - int32_t window_width = ANativeWindow_getWidth(g_Window); - int32_t window_height = ANativeWindow_getHeight(g_Window); - int display_width = window_width; - int display_height = window_height; - - io.DisplaySize = ImVec2((float)window_width, (float)window_height); - if (window_width > 0 && window_height > 0) - io.DisplayFramebufferScale = ImVec2((float)display_width / window_width, (float)display_height / window_height); - - // Setup time step - struct timespec current_timespec; - clock_gettime(CLOCK_MONOTONIC, ¤t_timespec); - double current_time = (double)(current_timespec.tv_sec) + (current_timespec.tv_nsec / 1000000000.0); - io.DeltaTime = g_Time > 0.0 ? (float)(current_time - g_Time) : (float)(1.0f / 60.0f); - g_Time = current_time; -} - -//----------------------------------------------------------------------------- - -#endif // #ifndef IMGUI_DISABLE diff --git a/backends/imgui_impl_android.h b/backends/imgui_impl_android.h index f6e41039a..f12cdf503 100644 --- a/backends/imgui_impl_android.h +++ b/backends/imgui_impl_android.h @@ -1,17 +1,34 @@ -// dear imgui: Platform Binding for Android native app -// This needs to be used along with the OpenGL 3 Renderer (imgui_impl_opengl3) +// dear imgui: Platform Backend for Android native app +// This needs to be used along with a Renderer Backend (e.g. OpenGL3, Vulkan) // Implemented features: // [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy AKEYCODE_* values are obsolete since 1.87 and not supported since 1.91.5] // [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen. +// [X] Platform: On-screen keyboard (soft input) — handled internally via JNI. No application code needed. (#3446) +// [X] Platform: Unicode character input — handled internally via JNI. No application code needed. (#3446) +// [X] Platform: Clipboard support (via JNI to Android ClipboardManager). (#7259) // Missing features or Issues: -// [ ] Platform: Clipboard support. // [ ] Platform: Gamepad support. // [ ] Platform: Mouse cursor shape and visibility (ImGuiBackendFlags_HasMouseCursors). Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. FIXME: Check if this is even possible with Android. // Important: // - Consider using SDL or GLFW backend on Android, which will be more full-featured than this. -// - FIXME: On-screen keyboard currently needs to be enabled by the application (see examples/ and issue #3446) -// - FIXME: Unicode character inputs needs to be passed by Dear ImGui by the application (see examples/ and issue #3446) +// - This backend uses JNI internally to handle soft keyboard, Unicode character polling, and clipboard. +// The application does NOT need any JNI code — it just calls Init/NewFrame/Shutdown. + +// BREAKING CHANGE: ImGui_ImplAndroid_Init() signature changed (#3446) +// +// Before (v1.92 and earlier): +// ImGui_ImplAndroid_Init(g_App->window); +// // Application had to call ShowSoftKeyboardInput() and PollUnicodeChars() every frame. +// // Application had to implement JNI boilerplate for clipboard. +// +// After: +// ImGui_ImplAndroid_Init(g_App->window, app->activity->assetManager, app->activity->clazz); +// // The backend handles soft keyboard, Unicode input, and clipboard internally. +// // Pass nullptr for asset_manager and/or native_activity to disable those features. +// +// To keep old behavior (no JNI features): +// ImGui_ImplAndroid_Init(g_App->window, nullptr, nullptr); // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. @@ -27,11 +44,40 @@ struct ANativeWindow; struct AInputEvent; +struct AAssetManager; + +// Forward declaration so users don't need JNI headers in their own includes. +// The actual type is 'jobject' from . If you pass a real jobject, include before this header. +#if __ANDROID__ +# include +#else +typedef void* jobject; +#endif + +// Display metrics returned by ImGui_ImplAndroid_GetDisplayMetrics(). +struct ImGui_ImplAndroid_DisplayMetrics +{ + int WidthPixels; + int HeightPixels; + float Density; // Logical density (1.0 = mdpi, 2.0 = xhdpi, 3.0 = xxhdpi) + int DensityDpi; // Physical dots per inch + float Xdpi; // Exact physical pixels per inch X + float Ydpi; // Exact physical pixels per inch Y + int Orientation; // 0=portrait, 1=landscape, 2=reverse portrait, 3=reverse landscape + float RefreshRate; // Screen refresh rate in Hz +}; // Follow "Getting Started" link and check examples/ folder to learn about using backends! -IMGUI_IMPL_API bool ImGui_ImplAndroid_Init(ANativeWindow* window); +IMGUI_IMPL_API bool ImGui_ImplAndroid_Init(ANativeWindow* window, struct AAssetManager* asset_manager = nullptr, jobject native_activity = nullptr); IMGUI_IMPL_API int32_t ImGui_ImplAndroid_HandleInputEvent(const AInputEvent* input_event); IMGUI_IMPL_API void ImGui_ImplAndroid_Shutdown(); IMGUI_IMPL_API void ImGui_ImplAndroid_NewFrame(); +// Optional: explicitly show/hide the soft keyboard (normally handled automatically in NewFrame based on io.WantTextInput) +IMGUI_IMPL_API void ImGui_ImplAndroid_ShowSoftKeyboard(); +IMGUI_IMPL_API void ImGui_ImplAndroid_HideSoftKeyboard(); + +// Display metrics — queried once during Init and refreshed in NewFrame if the window resizes. +IMGUI_IMPL_API void ImGui_ImplAndroid_GetDisplayMetrics(ImGui_ImplAndroid_DisplayMetrics* out_metrics); + #endif // #ifndef IMGUI_DISABLE diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 5cecbdb37..053503476 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -60,45 +60,6 @@ Other Changes: - Fixed double-click collapse toggle not owning the mouse button. If a `SetNextWindowPos()` with pivot was queued in the same frame, the second click could trigger another item in the same window. (#9439) [@Cleroth] -- Added `ImGuiItemFlags_LiveEditOnInputText` and `ImGuiItemFlags_LiveEditOnInputScalar` - flags to configure the timing of applying edits of backing variables when typing - values using a keyboard. - (#701, #9476, #3936, #3946, #5904, #6284, #8149, #8065, #8665, #9117, #9299, #700, #1351, - #1875, #2060, #2215, #2380, #2550, #3083, #3338, #3556, #4373, #4714, #4885, #5184, - #5777, #6707, #6766, #8004, #8303, #8915, #9308) - - Until now: - - Edits where always applied immediately to backing variable, which is equivalent - to the `ImGuiItemFlags_LiveEditXXX` flags being enabled. - - Typing '123' in an integer field would output successively 1, 12 then 123. - - Most uses of `IsItemDeactivatedAfterEdit()` or `ImGuiInputTextFlags_EnterReturnsTrue` - were actually workarounds for this issue. Advanced applications would typically - use `IsItemDeactivatedAfterEdit()` to distinguish transactions. - Workarounds often required a backing store for scalar values, and there were - also a few niggles related to `IsItemDeactivatedAfterEdit()` when using +/- - buttons of an `InputInt()` widgets. - Many of those situations can now be naturally simplified by disabling - `ImGuiItemFlags_LiveEditOnInputScalar`, which is expected to become the default. - - The new flags allows disabling this behavior selectively for strings fields - such as `InputText()` vs scalar fields: `SliderInt()`, `InputFloat()`, etc. - - When LiveEdit is disabled, edits are applied when pressing enter, tabbing out, - clearing a field or deactivating due to a focus loss. - - The flag may be altered programmatically: - PushItemFlag(ImGuiItemFlags_LiveEditOnInputScalar, false); // Disable for scalars - SliderInt(...); - PopItemFlag(); - PushItemFlag(ImGuiItemFlags_LiveEditOnInput, true); // Enable for all - SliderInt(...); - PopItemFlag(); - But with upcoming new defaults it is expected you shouldn't touch them much. - - Both flags currently defaults to true, which matches previous behavior. - - The expectation is that for strings/text, enabling LiveEdit is a better default. - - The expectation is that for scalars, disable LiveEdit is a better default. - - The short-term intent is to change `ImGuiItemFlags_LiveEditOnInputScalar` to - default to being disabled, as soon as we get more feedback from users (SOON). - - We intentionally are not adding `io.ConfigLiveEditXXX` fields to dictate the - default value of each `ImGuiItemFlags_LiveEditXXX`, because this is not - expected to be a user preference but a programmer/widget preferences. - Also, we strive to make the toolkit consistent. - InputText: - Added `style.InputTextCursorSize` to configure cursor/caret thickness. (#7031, #9409) This is automatically scaled by `style.ScaleAllSizes()`. @@ -189,25 +150,20 @@ Other Changes: - Misc: - Added IM_DEBUG_BREAK() handler for GCC+AArch64/ARM64. [@tom-seddon] - Backends: - - Android: - - Clear mouse position on touch release (AMOTION_EVENT_ACTION_UP) to prevent - items from staying in hovered state. (#6627, #9474) [@Turtle-PB] + - Android: Moved JNI soft-keyboard and Unicode character polling from the + example into the backend. The application no longer needs any JNI boilerplate. + Added clipboard support via JNI. Added display metrics (DPI, density, refresh + rate, orientation) via JNI, used for main_scale in the example. + BREAKING CHANGE: ImGui_ImplAndroid_Init() now takes additional asset_manager + and native_activity parameters. (#3446, #7259) - Metal4: - Added new Metal 4 backend (forked from Metal 3 backend). (#9458, #9451) [@AmelieHeinrich] - Added Metal-cpp support enabled with `IMGUI_IMPL_METAL_CPP` define. (#9461) [@MERL10N] - - OpenGL2: - - Backup and restore GL_UNPACK_ROW_LENGTH and GL_UNPACK_ALIGNMENT when updating texture - to avoid altering caller GL state. (#8802, #9473) [@Turtle-PB] - OpenGL3: - GLSL version detection assume GLSL 410 when GL context is 4.1. Fixes an issue running on macOS with Wine. [#9427, #6577) [@perminovVS] - Expose selected render state in ImGui_ImplOpenGL3_RenderState, allowing to dynamically select between use of glBindSampler() and glTexParameter(). (#9378) - - Backup and restore GL_UNPACK_ROW_LENGTH and GL_UNPACK_ALIGNMENT when updating texture - to avoid altering caller GL state. (#8802, #9473) [@Turtle-PB] - - SDL2: - - Restore SDL_StartTextInput()/SDL_StopTextInput() in IME handler for on-screen keyboard - support on Android. (#7636, #9474) [@Turtle-PB] - SDLRenderer3: - Fixed sampler change which didn't work on all graphics backends. (#7616, #9470, #9378) - Fixed default sampler not being Linear. Regression in 1.92.8. (#7616, #9470, #9378) [@ShiroKSH] @@ -216,6 +172,9 @@ Other Changes: when available, fixing OpenGL DPI scaling issues as e.g. NVIDIA drivers tends to spawn multiple-thread to manage OpenGL. (#9403) - Examples: + - Android: Removed JNI boilerplate (ShowSoftKeyboardInput, PollUnicodeChars) from + the example as these are now handled by the backend. (#3446) + Use display density for main_scale instead of hardcoded 2.0f. - Android: update to AGP 9.2.0 to support Gradle 9.6.0. - Apple+Metal4: added new example. (#9465, #9451) [@hoffstadt] - OpenGL3+GLFW/SDL2/SDL3: allow Wine compatibility by passing empty GLSL version diff --git a/examples/example_android_opengl3/CMakeLists.txt b/examples/example_android_opengl3/CMakeLists.txt index 63531f4dc..199bacd98 100644 --- a/examples/example_android_opengl3/CMakeLists.txt +++ b/examples/example_android_opengl3/CMakeLists.txt @@ -37,4 +37,5 @@ target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE EGL GLESv3 log + sensor ) diff --git a/examples/example_android_opengl3/android/app/src/main/java/MainActivity.kt b/examples/example_android_opengl3/android/app/src/main/java/MainActivity.kt index 896a88c8b..7f3f3c098 100644 --- a/examples/example_android_opengl3/android/app/src/main/java/MainActivity.kt +++ b/examples/example_android_opengl3/android/app/src/main/java/MainActivity.kt @@ -1,40 +1,40 @@ package imgui.example.android import android.app.NativeActivity -import android.os.Bundle import android.content.Context import android.view.inputmethod.InputMethodManager import android.view.KeyEvent import java.util.concurrent.LinkedBlockingQueue +// Minimal Kotlin activity extending NativeActivity. +// The three methods below (showSoftInput, hideSoftInput, pollUnicodeChar) are called +// from C++ via JNI by imgui_impl_android.cpp. They are required because Android's +// native API does not provide: (1) showing/hiding the soft keyboard, (2) getting +// Unicode characters from key events. +// +// The application developer does NOT write this code — it ships with the imgui +// example and is self-contained. The imgui_impl_android backend calls these +// methods automatically. class MainActivity : NativeActivity() { - public override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - } + private var unicodeCharQueue: LinkedBlockingQueue = LinkedBlockingQueue() fun showSoftInput() { - val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.showSoftInput(this.window.decorView, 0) + val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(window.decorView, 0) } fun hideSoftInput() { - val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.hideSoftInputFromWindow(this.window.decorView.windowToken, 0) + val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(window.decorView.windowToken, 0) } - // Queue for the Unicode characters to be polled from native code (via pollUnicodeChar()) - private var unicodeCharacterQueue: LinkedBlockingQueue = LinkedBlockingQueue() - - // We assume dispatchKeyEvent() of the NativeActivity is actually called for every - // KeyEvent and not consumed by any View before it reaches here + // Native Android key events don't expose getUnicodeChar() to C code. + // We intercept them here and queue the Unicode chars for the C++ backend to poll. override fun dispatchKeyEvent(event: KeyEvent): Boolean { - if (event.action == KeyEvent.ACTION_DOWN) { - unicodeCharacterQueue.offer(event.getUnicodeChar(event.metaState)) - } + if (event.action == KeyEvent.ACTION_DOWN) + unicodeCharQueue.offer(event.getUnicodeChar(event.metaState)) return super.dispatchKeyEvent(event) } - fun pollUnicodeChar(): Int { - return unicodeCharacterQueue.poll() ?: 0 - } + fun pollUnicodeChar(): Int = unicodeCharQueue.poll() ?: 0 } diff --git a/examples/example_android_opengl3/main.cpp b/examples/example_android_opengl3/main.cpp index 382006fa0..15fa1e19e 100644 --- a/examples/example_android_opengl3/main.cpp +++ b/examples/example_android_opengl3/main.cpp @@ -29,8 +29,6 @@ static std::string g_IniFilename = ""; static void Init(struct android_app* app); static void Shutdown(); static void MainLoopStep(); -static int ShowSoftKeyboardInput(); -static int PollUnicodeChars(); static int GetAssetData(const char* filename, void** out_data); // Main code @@ -148,11 +146,13 @@ void Init(struct android_app* app) //ImGui::StyleColorsLight(); // Setup Platform/Renderer backends - ImGui_ImplAndroid_Init(g_App->window); + ImGui_ImplAndroid_Init(g_App->window, app->activity->assetManager, app->activity->clazz); ImGui_ImplOpenGL3_Init("#version 300 es"); // Setup scaling - float main_scale = 2.0f; + ImGui_ImplAndroid_DisplayMetrics display_metrics; + ImGui_ImplAndroid_GetDisplayMetrics(&display_metrics); + float main_scale = display_metrics.Density > 0.0f ? display_metrics.Density : 2.0f; ImGuiStyle& style = ImGui::GetStyle(); style.ScaleAllSizes(main_scale); // Bake a fixed style scale. (until we have a solution for dynamic style scaling, changing this requires resetting Style + calling this again) style.FontScaleDpi = main_scale; // Set initial font scale. @@ -205,16 +205,6 @@ void MainLoopStep() static bool show_another_window = false; static ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); - // Poll Unicode characters via JNI - // FIXME: do not call this every frame because of JNI overhead - PollUnicodeChars(); - - // Open on-screen (soft) input if requested by Dear ImGui - static bool WantTextInputLast = false; - if (io.WantTextInput && !WantTextInputLast) - ShowSoftKeyboardInput(); - WantTextInputLast = io.WantTextInput; - // Start the Dear ImGui frame ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplAndroid_NewFrame(); @@ -297,77 +287,6 @@ void Shutdown() g_Initialized = false; } -// Helper functions - -// Unfortunately, there is no way to show the on-screen input from native code. -// Therefore, we call ShowSoftKeyboardInput() of the main activity implemented in MainActivity.kt via JNI. -static int ShowSoftKeyboardInput() -{ - JavaVM* java_vm = g_App->activity->vm; - JNIEnv* java_env = nullptr; - - jint jni_return = java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6); - if (jni_return == JNI_ERR) - return -1; - - jni_return = java_vm->AttachCurrentThread(&java_env, nullptr); - if (jni_return != JNI_OK) - return -2; - - jclass native_activity_clazz = java_env->GetObjectClass(g_App->activity->clazz); - if (native_activity_clazz == nullptr) - return -3; - - jmethodID method_id = java_env->GetMethodID(native_activity_clazz, "showSoftInput", "()V"); - if (method_id == nullptr) - return -4; - - java_env->CallVoidMethod(g_App->activity->clazz, method_id); - - jni_return = java_vm->DetachCurrentThread(); - if (jni_return != JNI_OK) - return -5; - - return 0; -} - -// Unfortunately, the native KeyEvent implementation has no getUnicodeChar() function. -// Therefore, we implement the processing of KeyEvents in MainActivity.kt and poll -// the resulting Unicode characters here via JNI and send them to Dear ImGui. -static int PollUnicodeChars() -{ - JavaVM* java_vm = g_App->activity->vm; - JNIEnv* java_env = nullptr; - - jint jni_return = java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6); - if (jni_return == JNI_ERR) - return -1; - - jni_return = java_vm->AttachCurrentThread(&java_env, nullptr); - if (jni_return != JNI_OK) - return -2; - - jclass native_activity_clazz = java_env->GetObjectClass(g_App->activity->clazz); - if (native_activity_clazz == nullptr) - return -3; - - jmethodID method_id = java_env->GetMethodID(native_activity_clazz, "pollUnicodeChar", "()I"); - if (method_id == nullptr) - return -4; - - // Send the actual characters to Dear ImGui - ImGuiIO& io = ImGui::GetIO(); - jint unicode_character; - while ((unicode_character = java_env->CallIntMethod(g_App->activity->clazz, method_id)) != 0) - io.AddInputCharacter(unicode_character); - - jni_return = java_vm->DetachCurrentThread(); - if (jni_return != JNI_OK) - return -5; - - return 0; -} - // Helper to retrieve data placed into the assets/ directory (android/app/src/main/assets) static int GetAssetData(const char* filename, void** outData) {