From 4a2883b4e2d1e94ab6a1a04c8c057167f2b0e518 Mon Sep 17 00:00:00 2001 From: Daijiro Fukuda Date: Mon, 10 Apr 2023 11:05:26 +0900 Subject: [PATCH] Win32: Support preedit candidate feature You can use this feature when you need to manage the drawing of the preedit candidates on the application side. --- README.md | 7 +++ docs/input.md | 74 ++++++++++++++++++++++ docs/intro.md | 31 +++++++--- include/GLFW/glfw3.h | 101 ++++++++++++++++++++++++++++++ src/init.c | 4 ++ src/input.c | 39 ++++++++++++ src/internal.h | 68 +++++++++++++------- src/win32_init.c | 2 + src/win32_platform.h | 3 + src/win32_window.c | 143 ++++++++++++++++++++++++++++++++++++++++++- tests/input_text.c | 77 ++++++++++++++++++++--- 11 files changed, 503 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 7cd73568..8cfce3eb 100644 --- a/README.md +++ b/README.md @@ -140,10 +140,17 @@ information on what to include when reporting a bug. area (#2130) - Added `glfwResetPreeditText` function to reset preedit of input method (#2130) + - Added `glfwSetPreeditCandidateCallback` function and + `GLFWpreeditcandidatefun` type for preedit candidates (#2130) + - Added `glfwGetPreeditCandidate` function to get a preeidt candidate text + (#2130) - Added `GLFW_IME` input mode for `glfwGetInputMode` and `glfwSetInputMode` (#2130) - Added `GLFW_X11_ONTHESPOT` init hint for using on-the-spot input method style on X11 (#2130) + - Added `GLFW_MANAGE_PREEDIT_CANDIDATE` init hint for displaying preedit + candidates on the application side (supported only on Windows currently) + (#2130) ## Contact diff --git a/docs/input.md b/docs/input.md index 3e881e84..a502ffa9 100644 --- a/docs/input.md +++ b/docs/input.md @@ -414,6 +414,80 @@ glfwResetPreeditText(window); @endcode +@subsection manage_preedit_candidate Manage preedit candidate + +By default, the IME manages the drawing of the preedit candidates, but +sometimes you need to do that on the application side for some reason. In such +a case, you can use +[GLFW_MANAGE_PREEDIT_CANDIDATE](@ref GLFW_MANAGE_PREEDIT_CANDIDATE_hint) init hint. +By setting this to `GLFW_TRUE`, the IME stops managing the drawing of the +candidates and the application needs to manage it by using the following +functions. + +@note +@win32 Only the OS currently supports this hint. + +You can register the candidate callback as follows. + +@code +glfwSetPreeditCandidateCallback(window, candidate_callback); +@endcode + +The callback receives the following information. + +@code +void candidate_callback(GLFWwindow* window, + int candidates_count, + int selected_index, + int page_start, + int page_size) +{ +} +@endcode + +`candidates_count` is the number of total candidates. `selected_index` is the +index of the currently selected candidate. Normally all candidates should not +be displayed at once, but divided into pages. You can use `page_start` and +`page_size` to manage the pages. `page_start` is the index of the first +candidate on the current page. `page_size` is the number of the candidates on +the current page. + +You can get the text of the candidate on the specific index as follows. Each +character of the returned text is a native endian UTF-32. + +@code +int text_count; +unsigned int* text = glfwGetPreeditCandidate(window, index, &text_count); +@endcode + +A sample code to get all candidate texts on the current page is as follows. + +@code +void candidate_callback(GLFWwindow* window, int candidates_count, + int selected_index, int page_start, int page_size) +{ + int i, j; + for (i = 0; i < page_size; ++i) + { + int index = i + page_start; + int text_count; + unsigned int* text = glfwGetPreeditCandidate(window, index, &text_count); + if (index == selected_index) + printf("> "); + for (j = 0; j < text_count; ++j) + { + char encoded[5] = ""; + encode_utf8(encoded, text[j]); // Some kind of encoding process + printf("%s", encoded); + } + printf("\n"); + } +} + +glfwSetPreeditCandidateCallback(window, candidate_callback); +@endcode + + ## Mouse input {#input_mouse} Mouse input comes in many forms, including mouse motion, button presses and diff --git a/docs/intro.md b/docs/intro.md index 486b9856..9004460e 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -118,6 +118,16 @@ The ANGLE platform type is specified via the `EGL_ANGLE_platform_angle` extension. This extension is not used if this hint is `GLFW_ANGLE_PLATFORM_TYPE_NONE`, which is the default value. +@anchor GLFW_MANAGE_PREEDIT_CANDIDATE_hint +__GLFW_MANAGE_PREEDIT_CANDIDATE__ specifies whether to manage the preedit +candidates on the application side. Possible values are `GLFW_TRUE` and +`GLFW_FALSE`. The default is `GLFW_FALSE` and there is no need to manage +the candidates on the application side. When you need to do that on the +application side for some reason, you can enable this hint. Please see +@ref ime_support for more information about IME support. + +@win32 Only the OS currently supports this hint. + #### macOS specific init hints {#init_hints_osx} @@ -164,16 +174,17 @@ it is recommended not to use this hint in normal cases. Possible values are #### Supported and default values {#init_hints_values} -Initialization hint | Default value | Supported values --------------------------------- | ------------------------------- | ---------------- -@ref GLFW_PLATFORM | `GLFW_ANY_PLATFORM` | `GLFW_ANY_PLATFORM`, `GLFW_PLATFORM_WIN32`, `GLFW_PLATFORM_COCOA`, `GLFW_PLATFORM_WAYLAND`, `GLFW_PLATFORM_X11` or `GLFW_PLATFORM_NULL` -@ref GLFW_JOYSTICK_HAT_BUTTONS | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` -@ref GLFW_ANGLE_PLATFORM_TYPE | `GLFW_ANGLE_PLATFORM_TYPE_NONE` | `GLFW_ANGLE_PLATFORM_TYPE_NONE`, `GLFW_ANGLE_PLATFORM_TYPE_OPENGL`, `GLFW_ANGLE_PLATFORM_TYPE_OPENGLES`, `GLFW_ANGLE_PLATFORM_TYPE_D3D9`, `GLFW_ANGLE_PLATFORM_TYPE_D3D11`, `GLFW_ANGLE_PLATFORM_TYPE_VULKAN` or `GLFW_ANGLE_PLATFORM_TYPE_METAL` -@ref GLFW_COCOA_CHDIR_RESOURCES | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` -@ref GLFW_COCOA_MENUBAR | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` -@ref GLFW_WAYLAND_LIBDECOR | `GLFW_WAYLAND_PREFER_LIBDECOR` | `GLFW_WAYLAND_PREFER_LIBDECOR` or `GLFW_WAYLAND_DISABLE_LIBDECOR` -@ref GLFW_X11_XCB_VULKAN_SURFACE | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` -@ref GLFW_X11_ONTHESPOT | `GLFW_FALSE` | `GLFW_TRUE` or `GLFW_FALSE` +Initialization hint | Default value | Supported values +---------------------------------- | ------------------------------- | ---------------- +@ref GLFW_PLATFORM | `GLFW_ANY_PLATFORM` | `GLFW_ANY_PLATFORM`, `GLFW_PLATFORM_WIN32`, `GLFW_PLATFORM_COCOA`, `GLFW_PLATFORM_WAYLAND`, `GLFW_PLATFORM_X11` or `GLFW_PLATFORM_NULL` +@ref GLFW_JOYSTICK_HAT_BUTTONS | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` +@ref GLFW_ANGLE_PLATFORM_TYPE | `GLFW_ANGLE_PLATFORM_TYPE_NONE` | `GLFW_ANGLE_PLATFORM_TYPE_NONE`, `GLFW_ANGLE_PLATFORM_TYPE_OPENGL`, `GLFW_ANGLE_PLATFORM_TYPE_OPENGLES`, `GLFW_ANGLE_PLATFORM_TYPE_D3D9`, `GLFW_ANGLE_PLATFORM_TYPE_D3D11`, `GLFW_ANGLE_PLATFORM_TYPE_VULKAN` or `GLFW_ANGLE_PLATFORM_TYPE_METAL` +@ref GLFW_MANAGE_PREEDIT_CANDIDATE | `GLFW_FALSE` | `GLFW_TRUE` or `GLFW_FALSE` +@ref GLFW_COCOA_CHDIR_RESOURCES | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` +@ref GLFW_COCOA_MENUBAR | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` +@ref GLFW_WAYLAND_LIBDECOR | `GLFW_WAYLAND_PREFER_LIBDECOR` | `GLFW_WAYLAND_PREFER_LIBDECOR` or `GLFW_WAYLAND_DISABLE_LIBDECOR` +@ref GLFW_X11_XCB_VULKAN_SURFACE | `GLFW_TRUE` | `GLFW_TRUE` or `GLFW_FALSE` +@ref GLFW_X11_ONTHESPOT | `GLFW_FALSE` | `GLFW_TRUE` or `GLFW_FALSE` ### Runtime platform selection {#platform} diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index 5b4df635..4f388966 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -1309,6 +1309,11 @@ extern "C" { * Platform selection [init hint](@ref GLFW_PLATFORM). */ #define GLFW_PLATFORM 0x00050003 +/*! @brief Preedit candidate init hint. + * + * Preedit candidate [init hint](@ref GLFW_MANAGE_PREEDIT_CANDIDATE_hint). + */ +#define GLFW_MANAGE_PREEDIT_CANDIDATE 0x00050004 /*! @brief macOS specific init hint. * * macOS specific [init hint](@ref GLFW_COCOA_CHDIR_RESOURCES_hint). @@ -1989,6 +1994,29 @@ typedef void (* GLFWpreeditfun)(GLFWwindow* window, */ typedef void (* GLFWimestatusfun)(GLFWwindow* window); +/*! @brief The function pointer type for preedit candidate callbacks. + * + * This is the function pointer type for preedit candidate callback functions. + * Use @ref glfwGetPreeditCandidate to get the candidate text for a specific index. + * + * @param[in] window The window that received the event. + * @param[in] candidates_count Candidates count. + * @param[in] selected_index.Index of selected candidate. + * @param[in] page_start Start index of candidate currently displayed. + * @param[in] page_size Count of candidates currently displayed. + * + * @sa @ref ime_support + * @sa @ref glfwSetPreeditCandidateCallback + * @sa @ref glfwGetPreeditCandidate + * + * @ingroup input + */ +typedef void (* GLFWpreeditcandidatefun)(GLFWwindow* window, + int candidates_count, + int selected_index, + int page_start, + int page_size); + /*! @brief The function pointer type for path drop callbacks. * * This is the function pointer type for path drop callbacks. A path drop @@ -5270,6 +5298,34 @@ GLFWAPI void glfwSetPreeditCursorRectangle(GLFWwindow* window, int x, int y, int */ GLFWAPI void glfwResetPreeditText(GLFWwindow* window); +/*! @brief Returns the preedit candidate. + * + * This function returns the text and the text-count of the preedit candidate. + * + * By default, the IME manages the preedit candidates, so there is no need to + * use this function. See @ref glfwSetPreeditCandidateCallback and + * [GLFW_MANAGE_PREEDIT_CANDIDATE](@ref GLFW_MANAGE_PREEDIT_CANDIDATE_hint) for details. + * + * @param[in] window The window. + * @param[in] index The index of the candidate. + * @param[out] textCount The text-count of the candidate. + * @return The text of the candidate as Unicode code points. + * + * @remark @macos @x11 @wayland Don't support this function. + * + * @par Thread Safety + * This function may only be called from the main thread. + * + * @sa @ref ime_support + * @sa @ref glfwSetPreeditCandidateCallback + * @sa [GLFW_MANAGE_PREEDIT_CANDIDATE](@ref GLFW_MANAGE_PREEDIT_CANDIDATE_hint) + * + * @since Added in GLFW 3.X. + * + * @ingroup input + */ +GLFWAPI unsigned int* glfwGetPreeditCandidate(GLFWwindow* window, int index, int* textCount); + /*! @brief Sets the key callback. * * This function sets the key callback of the specified window, which is called @@ -5478,6 +5534,51 @@ GLFWAPI GLFWpreeditfun glfwSetPreeditCallback(GLFWwindow* window, GLFWpreeditfun */ GLFWAPI GLFWimestatusfun glfwSetIMEStatusCallback(GLFWwindow* window, GLFWimestatusfun cbfun); +/*! @brief Sets the preedit candidate change callback. + * + * This function sets the preedit candidate callback of the specified + * window, which is called when the candidates are updated and can be used + * to display them by the application side. + * + * By default, this callback is not called because the IME displays the + * candidates and there is nothing to do on the application side. Only when + * the application side needs to use this to manage the displaying of + * IME candidates, you can set + * [GLFW_MANAGE_PREEDIT_CANDIDATE](@ref GLFW_MANAGE_PREEDIT_CANDIDATE_hint) init hint + * and stop the IME from managing it. + * + * @param[in] window The window whose callback to set. + * @param[in] cbfun The new callback, or `NULL` to remove the currently set + * callback. + * @return The previously set callback, or `NULL` if no callback was set or an + * error occurred. + * + * @callback_signature + * @code + * void function_name(GLFWwindow* window, + int candidates_count, + int selected_index, + int page_start, + int page_size) + * @endcode + * For more information about the callback parameters, see the + * [function pointer type](@ref GLFWpreeditcandidatefun). + * + * @remark @macos @x11 @wayland Don't support this function. The callback is + * not called. + * + * @par Thread Safety + * This function may only be called from the main thread. + * + * @sa @ref ime_support + * @sa [GLFW_MANAGE_PREEDIT_CANDIDATE](@ref GLFW_MANAGE_PREEDIT_CANDIDATE_hint) + * + * @since Added in GLFW 3.X + * + * @ingroup input + */ +GLFWAPI GLFWpreeditcandidatefun glfwSetPreeditCandidateCallback(GLFWwindow* window, GLFWpreeditcandidatefun cbfun); + /*! @brief Sets the mouse button callback. * * This function sets the mouse button callback of the specified window, which diff --git a/src/init.c b/src/init.c index 6619b0ec..372f71a3 100644 --- a/src/init.c +++ b/src/init.c @@ -52,6 +52,7 @@ static _GLFWinitconfig _glfwInitHints = .hatButtons = GLFW_TRUE, .angleType = GLFW_ANGLE_PLATFORM_TYPE_NONE, .platformID = GLFW_ANY_PLATFORM, + .managePreeditCandidate = GLFW_FALSE, .vulkanLoader = NULL, .ns = { @@ -474,6 +475,9 @@ GLFWAPI void glfwInitHint(int hint, int value) case GLFW_PLATFORM: _glfwInitHints.platformID = value; return; + case GLFW_MANAGE_PREEDIT_CANDIDATE: + _glfwInitHints.managePreeditCandidate = value; + return; case GLFW_COCOA_CHDIR_RESOURCES: _glfwInitHints.ns.chdir = value; return; diff --git a/src/input.c b/src/input.c index be0d63c9..cd9d0da2 100644 --- a/src/input.c +++ b/src/input.c @@ -355,6 +355,21 @@ void _glfwInputIMEStatus(_GLFWwindow* window) } } +// Notifies shared code of a preedit candidate event +// +void _glfwInputPreeditCandidate(_GLFWwindow* window) +{ + if (window->callbacks.preeditCandidate) + { + _GLFWpreedit* preedit = &window->preedit; + window->callbacks.preeditCandidate((GLFWwindow*) window, + preedit->candidateCount, + preedit->candidateSelection, + preedit->candidatePageStart, + preedit->candidatePageSize); + } +} + // Notifies shared code of a scroll event // void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset) @@ -1029,6 +1044,21 @@ GLFWAPI void glfwResetPreeditText(GLFWwindow* handle) _glfw.platform.resetPreeditText(window); } +GLFWAPI unsigned int* glfwGetPreeditCandidate(GLFWwindow* handle, int index, int* textCount) +{ + _GLFWwindow* window = (_GLFWwindow*) handle; + _GLFWpreedit* preedit = &window->preedit; + + if (preedit->candidateCount <= index) + return NULL; + + if (textCount) + *textCount = preedit->candidates[index].textCount; + + + return preedit->candidates[index].text; +} + GLFWAPI GLFWkeyfun glfwSetKeyCallback(GLFWwindow* handle, GLFWkeyfun cbfun) { _GLFW_REQUIRE_INIT_OR_RETURN(NULL); @@ -1078,6 +1108,15 @@ GLFWAPI GLFWimestatusfun glfwSetIMEStatusCallback(GLFWwindow* handle, GLFWimesta return cbfun; } +GLFWAPI GLFWpreeditcandidatefun glfwSetPreeditCandidateCallback(GLFWwindow* handle, + GLFWpreeditcandidatefun cbfun) +{ + _GLFWwindow* window = (_GLFWwindow*) handle; + _GLFW_REQUIRE_INIT_OR_RETURN(NULL); + _GLFW_SWAP(GLFWpreeditcandidatefun, window->callbacks.preeditCandidate, cbfun); + return cbfun; +} + GLFWAPI GLFWmousebuttonfun glfwSetMouseButtonCallback(GLFWwindow* handle, GLFWmousebuttonfun cbfun) { diff --git a/src/internal.h b/src/internal.h index 893f780c..80db7916 100644 --- a/src/internal.h +++ b/src/internal.h @@ -63,23 +63,24 @@ typedef int GLFWbool; typedef void (*GLFWproc)(void); -typedef struct _GLFWerror _GLFWerror; -typedef struct _GLFWinitconfig _GLFWinitconfig; -typedef struct _GLFWwndconfig _GLFWwndconfig; -typedef struct _GLFWctxconfig _GLFWctxconfig; -typedef struct _GLFWfbconfig _GLFWfbconfig; -typedef struct _GLFWcontext _GLFWcontext; -typedef struct _GLFWpreedit _GLFWpreedit; -typedef struct _GLFWwindow _GLFWwindow; -typedef struct _GLFWplatform _GLFWplatform; -typedef struct _GLFWlibrary _GLFWlibrary; -typedef struct _GLFWmonitor _GLFWmonitor; -typedef struct _GLFWcursor _GLFWcursor; -typedef struct _GLFWmapelement _GLFWmapelement; -typedef struct _GLFWmapping _GLFWmapping; -typedef struct _GLFWjoystick _GLFWjoystick; -typedef struct _GLFWtls _GLFWtls; -typedef struct _GLFWmutex _GLFWmutex; +typedef struct _GLFWerror _GLFWerror; +typedef struct _GLFWinitconfig _GLFWinitconfig; +typedef struct _GLFWwndconfig _GLFWwndconfig; +typedef struct _GLFWctxconfig _GLFWctxconfig; +typedef struct _GLFWfbconfig _GLFWfbconfig; +typedef struct _GLFWcontext _GLFWcontext; +typedef struct _GLFWpreedit _GLFWpreedit; +typedef struct _GLFWpreeditcandidate _GLFWpreeditcandidate; +typedef struct _GLFWwindow _GLFWwindow; +typedef struct _GLFWplatform _GLFWplatform; +typedef struct _GLFWlibrary _GLFWlibrary; +typedef struct _GLFWmonitor _GLFWmonitor; +typedef struct _GLFWcursor _GLFWcursor; +typedef struct _GLFWmapelement _GLFWmapelement; +typedef struct _GLFWmapping _GLFWmapping; +typedef struct _GLFWjoystick _GLFWjoystick; +typedef struct _GLFWtls _GLFWtls; +typedef struct _GLFWmutex _GLFWmutex; #define GL_VERSION 0x1f02 #define GL_NONE 0 @@ -378,6 +379,7 @@ struct _GLFWinitconfig GLFWbool hatButtons; int angleType; int platformID; + GLFWbool managePreeditCandidate; PFN_vkGetInstanceProcAddr vulkanLoader; struct { GLFWbool menubar; @@ -530,16 +532,34 @@ struct _GLFWcontext // Preedit structure for Input Method Editor/Engine // struct _GLFWpreedit +{ + unsigned int* text; + int textCount; + int textBufferCount; + int* blockSizes; + int blockSizesCount; + int blockSizesBufferCount; + int focusedBlockIndex; + int caretIndex; + int cursorPosX, cursorPosY, cursorWidth, cursorHeight; + + // Used only when apps display candidates by themselves. + // Usually, OS displays them, so apps don't need to do it. + _GLFWpreeditcandidate* candidates; + int candidateCount; + int candidateBufferCount; + int candidateSelection; + int candidatePageStart; + int candidatePageSize; +}; + +// Preedit candidate structure +// +struct _GLFWpreeditcandidate { unsigned int* text; int textCount; int textBufferCount; - int* blockSizes; - int blockSizesCount; - int blockSizesBufferCount; - int focusedBlockIndex; - int caretIndex; - int cursorPosX, cursorPosY, cursorWidth, cursorHeight; }; // Window and context structure @@ -601,6 +621,7 @@ struct _GLFWwindow GLFWcharmodsfun charmods; GLFWpreeditfun preedit; GLFWimestatusfun imestatus; + GLFWpreeditcandidatefun preeditCandidate; GLFWdropfun drop; } callbacks; @@ -961,6 +982,7 @@ void _glfwInputChar(_GLFWwindow* window, uint32_t codepoint, int mods, GLFWbool plain); void _glfwInputPreedit(_GLFWwindow* window); void _glfwInputIMEStatus(_GLFWwindow* window); +void _glfwInputPreeditCandidate(_GLFWwindow* window); void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset); void _glfwInputMouseClick(_GLFWwindow* window, int button, int action, int mods); void _glfwInputCursorPos(_GLFWwindow* window, double xpos, double ypos); diff --git a/src/win32_init.c b/src/win32_init.c index a3cd0d82..8d0eb65e 100644 --- a/src/win32_init.c +++ b/src/win32_init.c @@ -170,6 +170,8 @@ static GLFWbool loadLibraries(void) _glfw.win32.imm32.instance = _glfwPlatformLoadModule("imm32.dll"); if (_glfw.win32.imm32.instance) { + _glfw.win32.imm32.ImmGetCandidateListW_ = (PFN_ImmGetCandidateListW) + _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmGetCandidateListW"); _glfw.win32.imm32.ImmGetCompositionStringW_ = (PFN_ImmGetCompositionStringW) _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmGetCompositionStringW"); _glfw.win32.imm32.ImmGetContext_ = (PFN_ImmGetContext) diff --git a/src/win32_platform.h b/src/win32_platform.h index aed08ca3..eae33340 100644 --- a/src/win32_platform.h +++ b/src/win32_platform.h @@ -318,6 +318,7 @@ typedef LONG (WINAPI * PFN_RtlVerifyVersionInfo)(OSVERSIONINFOEXW*,ULONG,ULONGLO #define RtlVerifyVersionInfo _glfw.win32.ntdll.RtlVerifyVersionInfo_ // imm32 function pointer typedefs +typedef DWORD (WINAPI * PFN_ImmGetCandidateListW)(HIMC,DWORD,LPCANDIDATELIST,DWORD); typedef LONG (WINAPI * PFN_ImmGetCompositionStringW)(HIMC,DWORD,LPVOID,DWORD); typedef HIMC (WINAPI * PFN_ImmGetContext)(HWND); typedef BOOL (WINAPI * PFN_ImmGetConversionStatus)(HIMC,LPDWORD,LPDWORD); @@ -327,6 +328,7 @@ typedef BOOL (WINAPI * PFN_ImmNotifyIME)(HIMC,DWORD,DWORD,DWORD); typedef BOOL (WINAPI * PFN_ImmReleaseContext)(HWND,HIMC); typedef BOOL (WINAPI * PFN_ImmSetCandidateWindow)(HIMC,LPCANDIDATEFORM); typedef BOOL (WINAPI * PFN_ImmSetOpenStatus)(HIMC,BOOL); +#define ImmGetCandidateListW _glfw.win32.imm32.ImmGetCandidateListW_ #define ImmGetCompositionStringW _glfw.win32.imm32.ImmGetCompositionStringW_ #define ImmGetContext _glfw.win32.imm32.ImmGetContext_ #define ImmGetConversionStatus _glfw.win32.imm32.ImmGetConversionStatus_ @@ -526,6 +528,7 @@ typedef struct _GLFWlibraryWin32 struct { HINSTANCE instance; + PFN_ImmGetCandidateListW ImmGetCandidateListW_; PFN_ImmGetCompositionStringW ImmGetCompositionStringW_; PFN_ImmGetContext ImmGetContext_; PFN_ImmGetConversionStatus ImmGetConversionStatus_; diff --git a/src/win32_window.c b/src/win32_window.c index eda2df6a..396c1771 100644 --- a/src/win32_window.c +++ b/src/win32_window.c @@ -35,6 +35,7 @@ #include #include #include +#include // Converts utf16 units to Unicode code points (UTF32). // Returns GLFW_TRUE when the converting completes and the result is assigned to @@ -564,6 +565,117 @@ static void maximizeWindowManually(_GLFWwindow* window) SWP_NOACTIVATE | SWP_NOZORDER | SWP_FRAMECHANGED); } +// Store candidate text from the buffer data +// +static void setCandidate(_GLFWpreeditcandidate* candidate, LPWSTR buffer) +{ + size_t bufferCount = wcslen(buffer); + int textBufferCount = candidate->textBufferCount; + uint32_t codepoint; + WCHAR highSurrogate = 0; + int convertedLength = 0; + int i; + + while ((size_t) textBufferCount < bufferCount + 1) + textBufferCount = (textBufferCount == 0) ? 1 : textBufferCount * 2; + if (textBufferCount != candidate->textBufferCount) + { + unsigned int* text = + _glfw_realloc(candidate->text, + sizeof(unsigned int) * textBufferCount); + if (text == NULL) + return; + candidate->text = text; + candidate->textBufferCount = textBufferCount; + } + + for (i = 0; (size_t) i < bufferCount; ++i) + { + if (convertToUTF32FromUTF16(buffer[i], + &highSurrogate, + &codepoint)) + candidate->text[convertedLength++] = codepoint; + } + + candidate->textCount = convertedLength; +} + +// Get preedit candidates of Imm32 and pass them to candidate-callback +// +static void getImmCandidates(_GLFWwindow* window) +{ + _GLFWpreedit* preedit = &window->preedit; + HIMC hIMC = ImmGetContext(window->win32.handle); + DWORD candidateListBytes = ImmGetCandidateListW(hIMC, 0, NULL, 0); + + if (candidateListBytes == 0) + { + ImmReleaseContext(window->win32.handle, hIMC); + return; + } + + { + int i; + int bufferCount = preedit->candidateBufferCount; + LPCANDIDATELIST candidateList = _glfw_calloc(candidateListBytes, 1); + if (candidateList == NULL) + { + ImmReleaseContext(window->win32.handle, hIMC); + return; + } + ImmGetCandidateListW(hIMC, 0, candidateList, candidateListBytes); + ImmReleaseContext(window->win32.handle, hIMC); + + while ((DWORD) bufferCount < candidateList->dwCount + 1) + bufferCount = (bufferCount == 0) ? 1 : bufferCount * 2; + if (bufferCount != preedit->candidateBufferCount) + { + _GLFWpreeditcandidate* candidates = + _glfw_realloc(preedit->candidates, + sizeof(_GLFWpreeditcandidate) * bufferCount); + if (candidates == NULL) + { + _glfw_free(candidateList); + return; + } + // `realloc` does not initialize the increased area with 0. + // This logic should be moved to a more appropriate place to share + // when other platforms support this feature. + for (i = preedit->candidateBufferCount; i < bufferCount; ++i) + { + candidates[i].text = NULL; + candidates[i].textCount = 0; + candidates[i].textBufferCount = 0; + } + preedit->candidates = candidates; + preedit->candidateBufferCount = bufferCount; + } + + for (i = 0; (DWORD) i < candidateList->dwCount; ++i) + setCandidate(&preedit->candidates[i], + (LPWSTR)((char*) candidateList + candidateList->dwOffset[i])); + + preedit->candidateCount = candidateList->dwCount; + preedit->candidateSelection = candidateList->dwSelection; + preedit->candidatePageStart = candidateList->dwPageStart; + preedit->candidatePageSize = candidateList->dwPageSize; + + _glfw_free(candidateList); + } + + _glfwInputPreeditCandidate(window); +} + +// Clear preedit candidates +static void clearImmCandidate(_GLFWwindow* window) +{ + window->preedit.candidateCount = 0; + window->preedit.candidateSelection = 0; + window->preedit.candidatePageStart = 0; + window->preedit.candidatePageSize = 0; + _glfwInputPreeditCandidate(window); +} + // Get preedit texts of Imm32 and pass them to preedit-callback // static GLFWbool getImmPreedit(_GLFWwindow* window) @@ -798,6 +910,12 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l // To draw preedit text by an application side if (lParam & ISC_SHOWUICOMPOSITIONWINDOW) lParam &= ~ISC_SHOWUICOMPOSITIONWINDOW; + + if (_glfw.hints.init.managePreeditCandidate && + (lParam & ISC_SHOWUICANDIDATEWINDOW)) + { + lParam &= ~ISC_SHOWUICANDIDATEWINDOW; + } break; } @@ -1044,15 +1162,34 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l case WM_IME_ENDCOMPOSITION: { clearImmPreedit(window); + // Usually clearing candidates in IMN_CLOSECANDIDATE is sufficient. + // However, some IME need it here, e.g. Google Japanese Input. + clearImmCandidate(window); return TRUE; } case WM_IME_NOTIFY: { - if (wParam == IMN_SETOPENSTATUS) + switch (wParam) { - _glfwInputIMEStatus(window); - return TRUE; + case IMN_SETOPENSTATUS: + { + _glfwInputIMEStatus(window); + return TRUE; + } + + case IMN_OPENCANDIDATE: + case IMN_CHANGECANDIDATE: + { + getImmCandidates(window); + return TRUE; + } + + case IMN_CLOSECANDIDATE: + { + clearImmCandidate(window); + return TRUE; + } } break; } diff --git a/tests/input_text.c b/tests/input_text.c index 3cce0227..b2030f80 100644 --- a/tests/input_text.c +++ b/tests/input_text.c @@ -119,11 +119,16 @@ static int currentIMEStatus = GLFW_FALSE; #define MAX_PREEDIT_LEN 128 static char preeditBuf[MAX_PREEDIT_LEN] = ""; +// Assuming that the page-size is 10 at most. +static char candidateBuf[9][MAX_PREEDIT_LEN]; +static int candidatePageSize = 0; + void usage(void) { - printf("Usage: input_text [-h] [-s]\n"); + printf("Usage: input_text [-h] [-s] [-c]\n"); printf("Options:\n"); printf(" -s Use on-the-spot sytle on X11. This is ignored on other platforms.\n"); + printf(" -c Use manage-preedit-candidate on Win32. This is ignored on other platforms.\n"); printf(" -h Show this help\n"); } @@ -566,11 +571,32 @@ static void set_preedit_labels(GLFWwindow* window, struct nk_context* nk, int he nk_layout_row_end(nk); } +static void set_candidate_labels(GLFWwindow* window, struct nk_context* nk) +{ + for (int i = 0; i < 5; ++i) + { + nk_layout_row_begin(nk, NK_DYNAMIC, 30, 3); + nk_layout_row_push(nk, 1.f / 3.f); + if (i == 0) + nk_label(nk, "Candidates:", NK_TEXT_LEFT); + else + nk_label(nk, "", NK_TEXT_LEFT); + nk_layout_row_push(nk, 1.f / 3.f); + if (candidatePageSize > i) + nk_label(nk, (const char*) candidateBuf[i], NK_TEXT_LEFT); + nk_layout_row_push(nk, 1.f / 3.f); + if (candidatePageSize > i + 5) + nk_label(nk, (const char*) candidateBuf[i + 5], NK_TEXT_LEFT); + nk_layout_row_end(nk); + } +} + // If it is possible to take the text-cursor position calculated in `nk_do_edit` function in `deps/nuklear.h`, // we can set preedit-cursor position more easily. // However, there doesn't seem to be a way to do that, so this does a simplified calculation only for the end // of the text. (Can not trace the cursor movement) -static void update_cursor_pos(GLFWwindow* window, struct nk_context* nk, struct nk_user_font* f, char* boxBuffer, int boxLen) +static void update_cursor_pos(GLFWwindow* window, struct nk_context* nk, struct nk_user_font* f, + char* boxBuffer, int boxLen, int boxX, int boxY) { float lineWidth = 0; int totalLines = 1; @@ -606,14 +632,10 @@ static void update_cursor_pos(GLFWwindow* window, struct nk_context* nk, struct } { - // I don't know how to get these info. - int widgetLayoutX = 10; - int widgetLayoutY = 220; - int lineHeight = f->height + nk->style.edit.row_padding; - int cursorPosX = widgetLayoutX + lineWidth; - int cursorPosY = widgetLayoutY + lineHeight * (totalLines - 1); + int cursorPosX = boxX + lineWidth; + int cursorPosY = boxY + lineHeight * (totalLines - 1); int cursorHeight = lineHeight; int cursorWidth; @@ -686,6 +708,30 @@ static void preedit_callback(GLFWwindow* window, int preeditCount, } } +static void candidate_callback(GLFWwindow* window, int candidates_count, + int selected_index, int page_start, int page_size) +{ + int i, j; + candidatePageSize = page_size; + for (i = 0; i < page_size; ++i) + { + int index = i + page_start; + int textCount; + unsigned int* text = glfwGetPreeditCandidate(window, index, &textCount); + if (index == selected_index) + strcpy(candidateBuf[i], "> "); + else + strcpy(candidateBuf[i], ""); + for (j = 0; j < textCount; ++j) + { + char encoded[5] = ""; + encode_utf8(encoded, text[j]); + if (strlen(candidateBuf[i]) + strlen(encoded) < MAX_PREEDIT_LEN) + strcat(candidateBuf[i], encoded); + } + } +} + int main(int argc, char** argv) { GLFWwindow* window; @@ -694,9 +740,10 @@ int main(int argc, char** argv) char boxBuffer[MAX_BUFFER_LEN] = "Input text here."; int boxLen = strlen(boxBuffer); int isAutoUpdatingCursorPosEnabled = GLFW_TRUE; + int managePreeditCandidate = GLFW_FALSE; int ch; - while ((ch = getopt(argc, argv, "hs")) != -1) + while ((ch = getopt(argc, argv, "hsc")) != -1) { switch (ch) { @@ -707,6 +754,11 @@ int main(int argc, char** argv) case 's': glfwInitHint(GLFW_X11_ONTHESPOT, GLFW_TRUE); break; + + case 'c': + glfwInitHint(GLFW_MANAGE_PREEDIT_CANDIDATE, GLFW_TRUE); + managePreeditCandidate = GLFW_TRUE; + break; } } @@ -729,6 +781,7 @@ int main(int argc, char** argv) glfwSetPreeditCursorRectangle(window, 0, 0, 1, 1); glfwSetIMEStatusCallback(window, ime_callback); glfwSetPreeditCallback(window, preedit_callback); + glfwSetPreeditCandidateCallback(window, candidate_callback); glfwMakeContextCurrent(window); gladLoadGL(glfwGetProcAddress); @@ -757,6 +810,8 @@ int main(int argc, char** argv) set_preedit_cursor_edit(window, nk, 30, &isAutoUpdatingCursorPosEnabled); set_ime_stauts_labels(window, nk, 30); set_preedit_labels(window, nk, 30); + if (managePreeditCandidate) + set_candidate_labels(window, nk); nk_layout_row_dynamic(nk, height - 250, 1); nk_edit_string(nk, NK_EDIT_BOX, boxBuffer, &boxLen, MAX_BUFFER_LEN, nk_filter_default); @@ -768,7 +823,9 @@ int main(int argc, char** argv) glfwSwapBuffers(window); if (isAutoUpdatingCursorPosEnabled) - update_cursor_pos(window, nk, ¤tFont->handle, boxBuffer, boxLen); + // I don't know how to get the layout info of `nk_edit_string`. + update_cursor_pos(window, nk, ¤tFont->handle, boxBuffer, boxLen, 10, + managePreeditCandidate ? 385 : 220); glfwWaitEvents(); }