diff --git a/.gitignore b/.gitignore index dd88e65e..7eb1d6ac 100644 --- a/.gitignore +++ b/.gitignore @@ -81,4 +81,3 @@ tests/timeout tests/title tests/vulkan tests/windows - diff --git a/CMakeLists.txt b/CMakeLists.txt index a480de37..2ae748e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -226,6 +226,7 @@ endif() if (_GLFW_WIN32) list(APPEND glfw_PKG_LIBS "-lgdi32") + list(APPEND glfw_LIBRARIES "imm32") if (GLFW_USE_HYBRID_HPG) set(_GLFW_USE_HYBRID_HPG 1) diff --git a/docs/input.dox b/docs/input.dox index 497fe676..8b448e81 100644 --- a/docs/input.dox +++ b/docs/input.dox @@ -206,6 +206,96 @@ void charmods_callback(GLFWwindow* window, unsigned int codepoint, int mods) } @endcode +@subsection preedit IME Support + +All desktop operating systems support IME (Input Method Editor) to input characters +that are not mapped with physical keys. IME have been popular among Eeastern Asian people. +And some operating systems start supporting voice input via IME mechanism. + +GLFW provides IME support functions to help +you implement better text input features. You should add suitable visualization code for +preedit text. + +IME works in front of actual character input events (@ref input_char). +If your application uses text input and you want to support IME, +you should register preedit callback to receive preedit text before committed. + +@code +glfwSetPreeditCallback(window, preedit_callback); +@endcode + +The callback function receives chunk of text and focused block information. + +@code +static void preedit_callback(GLFWwindow* window, int strLength, unsigned int* string, int blockLength, int* blocks, int focusedBlock) { +} +@endcode + +strLength and string parameter reprsent whole preedit text. Each character of the preedit string is a codepoint like @ref input_char. + +If you want to type the text "寿司(sushi)", Usually the callback is called several times like the following sequence: + +-# key event: s +-# preedit: [string: "s", block: [1], focusedBlock: 0] +-# key event: u +-# preedit: [string: "す", block: [1], focusedBlock: 0] +-# key event: s +-# preedit: [string: "すs", block: [2], focusedBlock: 0] +-# key event: h +-# preedit: [string: "すsh", block: [2], focusedBlock: 0] +-# key event: i +-# preedit: [string: "すし", block: [2], focusedBlock: 0] +-# key event: ' ' +-# preedit: [string: "寿司", block: [2], focusedBlock: 0] +-# char: '寿' +-# char: '司' +-# preedit: [string: "", block: [], focusedBlock: 0] + +If preedit text includes several semantic blocks, preedit callbacks returns several blocks after a space key pressed: + +-# preedit: [string: "わたしはすしをたべます", block: [11], focusedBlock: 0] +-# preedit: [string: "私は寿司を食べます", block: [2, 7], focusedBlock: 1] + +"blocks" is a list of block length. The above case, it contains the following blocks and second block is focused. + +- 私は +- [寿司を食べます] + +commited text(passed via regular @ref input_char event), unfocused block, focused block should have different text style. + + +GLFW provides helper function to teach suitable position of the candidate window to window system. +Window system decides the best position from text cursor geometry (x, y coords and height). You should call this function +in the above preedit text callback function. + +@code +glfwSetPreeditCursorPos(window, x, y, h); +glfwGetPreeditCursorPos(window, &x, &y, &h); +@endcode + +Sometimes IME task is interrupted by user or application. There are several functions to support these situation. +You can receive notification about IME status change(on/off) by using the following function: + +@code +glfwSetIMEStatusCallback(window, imestatus_callback); +@endcode + +imestatus_callback has simple sigunature like this: + +@code +static void imestatus_callback(GLFWwindow* window) { +} +@endcode + +You can implement the code that resets or commits preedit text when IME status is changed and preedit text is not empty. + +When the focus is gone from text box, you can use the following functions to reset IME status: + +@code +void glfwResetPreeditText(GLFWwindow* window); +void glfwSetIMEStatus(GLFWwindow* window, int active) +int glfwGetIMEStatus(GLFWwindow* window) +@endcode @subsection input_key_name Key names diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index 15ace888..be3dbd16 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -686,6 +686,7 @@ extern "C" { #define GLFW_CURSOR 0x00033001 #define GLFW_STICKY_KEYS 0x00033002 #define GLFW_STICKY_MOUSE_BUTTONS 0x00033003 +#define GLFW_IME 0x00033004 #define GLFW_CURSOR_NORMAL 0x00034001 #define GLFW_CURSOR_HIDDEN 0x00034002 @@ -1100,6 +1101,37 @@ typedef void (* GLFWcharfun)(GLFWwindow*,unsigned int); */ typedef void (* GLFWcharmodsfun)(GLFWwindow*,unsigned int,int); +/*! @brief The function signature for preedit callbacks. + * + * This is the function signature for preedit callback functions. + * + * @param[in] window The window that received the event. + * @param[in] length Preedit string length. + * @param[in] string Preedit string. + * @param[in] count Attributed block count. + * @param[in] blocksizes List of attributed block size. + * @param[in] focusedblock Focused block index. + * + * @sa @ref preedit + * @sa glfwSetPreeditCallback + * + * @ingroup input + */ +typedef void (* GLFWpreeditfun)(GLFWwindow*,int,unsigned int*,int,int*,int); + +/*! @brief The function signature for IME status change callbacks. + * + * This is the function signature for IME status change callback functions. + * + * @param[in] window The window that received the event. + * + * @sa @ref preedit + * @sa glfwSetIMEStatusCallback + * + * @ingroup monitor + */ +typedef void (* GLFWimestatusfun)(GLFWwindow*); + /*! @brief The function signature for file drop callbacks. * * This is the function signature for file drop callbacks. @@ -3009,12 +3041,12 @@ GLFWAPI void glfwPostEmptyEvent(void); /*! @brief Returns the value of an input option for the specified window. * * This function returns the value of an input option for the specified window. - * The mode must be one of `GLFW_CURSOR`, `GLFW_STICKY_KEYS` or - * `GLFW_STICKY_MOUSE_BUTTONS`. + * The mode must be one of `GLFW_CURSOR`, `GLFW_STICKY_KEYS`, + * `GLFW_STICKY_MOUSE_BUTTONS` or `GLFW_IME`. * * @param[in] window The window to query. - * @param[in] mode One of `GLFW_CURSOR`, `GLFW_STICKY_KEYS` or - * `GLFW_STICKY_MOUSE_BUTTONS`. + * @param[in] mode One of `GLFW_CURSOR`, `GLFW_STICKY_KEYS`, + * `GLFW_STICKY_MOUSE_BUTTONS` or `GLFW_IME`. * * @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref * GLFW_INVALID_ENUM. @@ -3032,8 +3064,8 @@ GLFWAPI int glfwGetInputMode(GLFWwindow* window, int mode); /*! @brief Sets an input option for the specified window. * * This function sets an input mode option for the specified window. The mode - * must be one of `GLFW_CURSOR`, `GLFW_STICKY_KEYS` or - * `GLFW_STICKY_MOUSE_BUTTONS`. + * must be one of `GLFW_CURSOR`, `GLFW_STICKY_KEYS`, + * `GLFW_STICKY_MOUSE_BUTTONS` or `GLFW_IME`. * * If the mode is `GLFW_CURSOR`, the value must be one of the following cursor * modes: @@ -3059,9 +3091,12 @@ GLFWAPI int glfwGetInputMode(GLFWwindow* window, int mode); * you are only interested in whether mouse buttons have been pressed but not * when or in which order. * + * If the mode is `GLFW_IME`, the value must be either `GLFW_TRUE` to turn on IME, + * or `GLFW_FALSE` to turn off it. + * * @param[in] window The window whose input mode to set. - * @param[in] mode One of `GLFW_CURSOR`, `GLFW_STICKY_KEYS` or - * `GLFW_STICKY_MOUSE_BUTTONS`. + * @param[in] mode One of `GLFW_CURSOR`, `GLFW_STICKY_KEYS`, + * `GLFW_STICKY_MOUSE_BUTTONS` or `GLFW_IME` * @param[in] value The new value of the specified input mode. * * @errors Possible errors include @ref GLFW_NOT_INITIALIZED, @ref @@ -3422,6 +3457,67 @@ GLFWAPI void glfwDestroyCursor(GLFWcursor* cursor); */ GLFWAPI void glfwSetCursor(GLFWwindow* window, GLFWcursor* cursor); +/*! @brief Retrieves the position of the text cursor relative to the client area of window. + * + * This function returns position hint to decide the candidate window. + * + * @param[in] window The window to set the text cursor for. + * @param[out] x The text cursor x position (relative position from window coordinates). + * @param[out] y The text cursor y position (relative position from window coordinates). + * @param[out] h The text cursor height. + * + * @par Thread Safety + * This function may only be called from the main thread. + * + * @sa @ref input_char + * + * @since Added in GLFW 3.X. + * + * @ingroup input + */ +GLFWAPI void glfwGetPreeditCursorPos(GLFWwindow* window, int *x, int *y, int *h); + +/*! @brief Notify the text cursor position to window system to decide the candidate window position. + * + * This function teach position hint to decide the candidate window. The candidate window + * is a part of IME(Input Method Editor) and show several candidate strings. + * + * Windows sytems decide proper pisition from text cursor geometry. + * You should call this function in preedit callback. + * + * @param[in] window The window to set the text cursor for. + * @param[in] x The text cursor x position (relative position from window coordinates). + * @param[in] y The text cursor y position (relative position from window coordinates). + * @param[in] h The text cursor height. + * + * @par Thread Safety + * This function may only be called from the main thread. + * + * @sa @ref input_char + * + * @since Added in GLFW 3.X. + * + * @ingroup input + */ +GLFWAPI void glfwSetPreeditCursorPos(GLFWwindow* window, int x, int y, int h); + +/*! @brief Reset IME input status. + * + * This function resets IME's preedit text. + * + * @param[in] window The window. + * + * @par Thread Safety + * This function may only be called from the main thread. + * + * @sa @ref preedit + * + * @since Added in GLFW 3.X. + * + * @ingroup input + */ +GLFWAPI void glfwResetPreeditText(GLFWwindow* window); + /*! @brief Sets the key callback. * * This function sets the key callback of the specified window, which is called @@ -3536,6 +3632,58 @@ GLFWAPI GLFWcharfun glfwSetCharCallback(GLFWwindow* window, GLFWcharfun cbfun); */ GLFWAPI GLFWcharmodsfun glfwSetCharModsCallback(GLFWwindow* window, GLFWcharmodsfun cbfun); +/*! @brief Sets the preedit callback. + * + * This function sets the preedit callback of the specified + * window, which is called when an IME is processing text before commited. + * + * Callback receives relative position of input cursor inside preedit text and + * attributed text blocks. This callback is used for on-the-spot text editing + * with IME. + * + * @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. + * + * @par Thread Safety + * This function may only be called from the main thread. + * + * @sa @ref input_char + * + * @since Added in GLFW 3.X + * + * @ingroup input + */ +GLFWAPI GLFWpreeditfun glfwSetPreeditCallback(GLFWwindow* window, GLFWpreeditfun cbfun); + +/*! @brief Sets the IME status change callback. + * + * This function sets the preedit callback of the specified + * window, which is called when an IME is processing text before commited. + * + * Callback receives relative position of input cursor inside preedit text and + * attributed text blocks. This callback is used for on-the-spot text editing + * with IME. + * + * @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. + * + * @par Thread Safety + * This function may only be called from the main thread. + * + * @sa @ref input_char + * + * @since Added in GLFW 3.X + * + * @ingroup input + */ +GLFWAPI GLFWimestatusfun glfwSetIMEStatusCallback(GLFWwindow* window, GLFWimestatusfun cbfun); + /*! @brief Sets the mouse button callback. * * This function sets the mouse button callback of the specified window, which diff --git a/src/cocoa_window.m b/src/cocoa_window.m index 93f1a203..eb63d3c8 100644 --- a/src/cocoa_window.m +++ b/src/cocoa_window.m @@ -316,8 +316,11 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; _glfwInputWindowFocus(window, GLFW_FALSE); } -@end +- (void)imeStatusChangeNotified:(NSNotification *)notification { + _glfwInputIMEStatus(window); +} +@end //------------------------------------------------------------------------ // Delegate for application related notifications @@ -714,6 +717,56 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; [markedText initWithAttributedString:string]; else [markedText initWithString:string]; + NSString* markedTextString = markedText.string; + + NSUInteger i, length = [markedTextString length]; + int ctext = window->ctext; + while (ctext < length+1) { + ctext = (ctext == 0) ? 1 : ctext*2; + } + if (ctext != window->ctext) { + unsigned int* preeditText = realloc(window->preeditText, sizeof(unsigned int)*ctext); + if (preeditText == NULL) { + return; + } + window->preeditText = preeditText; + window->ctext = ctext; + } + window->ntext = length; + window->preeditText[length] = 0; + for (i = 0; i < length; i++) + { + const unichar codepoint = [markedTextString characterAtIndex:i]; + window->preeditText[i] = codepoint; + } + int focusedBlock = 0; + NSInteger offset = 0; + window->nblocks = 0; + while (offset < length) { + NSRange effectiveRange; + NSDictionary *attributes = [markedText attributesAtIndex:offset effectiveRange:&effectiveRange]; + + if (window->nblocks == window->cblocks) { + int cblocks = window->cblocks * 2; + int* blocks = realloc(window->preeditAttributeBlocks, sizeof(int)*cblocks); + if (blocks == NULL) { + return; + } + window->preeditAttributeBlocks = blocks; + window->cblocks = cblocks; + } + window->preeditAttributeBlocks[window->nblocks] = effectiveRange.length; + offset += effectiveRange.length; + if (effectiveRange.length == 0) { + break; + } + NSNumber* underline = (NSNumber*) [attributes objectForKey:@"NSUnderline"]; + if ([underline intValue] != 1) { + focusedBlock = window->nblocks; + } + window->nblocks++; + } + _glfwInputPreedit(window, focusedBlock); } - (void)unmarkText @@ -742,8 +795,7 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; { int xpos, ypos; _glfwPlatformGetWindowPos(window, &xpos, &ypos); - const NSRect contentRect = [window->ns.view frame]; - return NSMakeRect(xpos, transformY(ypos + contentRect.size.height), 0.0, 0.0); + return NSMakeRect(xpos + window->preeditCursorPosX, transformY(ypos + window->preeditCursorPosY), 0.0, window->preeditCursorHeight); } - (void)insertText:(id)string replacementRange:(NSRange)replacementRange @@ -1049,6 +1101,11 @@ static GLFWbool createNativeWindow(_GLFWwindow* window, [window->ns.object setAcceptsMouseMovedEvents:YES]; [window->ns.object setRestorable:NO]; + [[NSNotificationCenter defaultCenter] + addObserver: window->ns.delegate + selector:@selector(imeStatusChangeNotified:) + name:NSTextInputContextKeyboardSelectionDidChangeNotification + object: nil]; return GLFW_TRUE; } @@ -1102,6 +1159,8 @@ void _glfwPlatformDestroyWindow(_GLFWwindow* window) if (_glfw.ns.disabledCursorWindow == window) _glfw.ns.disabledCursorWindow = NULL; + [[NSNotificationCenter defaultCenter] removeObserver: window->ns.delegate]; + [window->ns.object orderOut:nil]; if (window->monitor) @@ -1735,6 +1794,49 @@ VkResult _glfwPlatformCreateWindowSurface(VkInstance instance, #endif } +void _glfwPlatformResetPreeditText(_GLFWwindow* window) +{ + NSTextInputContext *context = [NSTextInputContext currentInputContext]; + [context discardMarkedText]; + [window->ns.view unmarkText]; +} + +void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active) +{ + // Mac OS has several input sources. + // this code assumes input methods not in ascii capable inputs using IME. + NSArray* asciiInputSources = CFBridgingRelease(TISCreateASCIICapableInputSourceList()); + TISInputSourceRef asciiSource = (__bridge TISInputSourceRef)([asciiInputSources firstObject]); + if (active) { + NSArray* allInputSources = CFBridgingRelease(TISCreateInputSourceList(NULL, false)); + NSString* asciiSourceID = (__bridge NSString *)(TISGetInputSourceProperty(asciiSource, kTISPropertyInputSourceID)); + int i; + int count = [allInputSources count]; + for (i = 0; i < count; i++) { + TISInputSourceRef source = (__bridge TISInputSourceRef)([allInputSources objectAtIndex: i]); + NSString* sourceID = (__bridge NSString *)(TISGetInputSourceProperty(source, kTISPropertyInputSourceID)); + if ([asciiSourceID compare: sourceID] != NSOrderedSame) { + TISSelectInputSource(source); + break; + } + } + } else if (asciiSource) { + TISSelectInputSource(asciiSource); + } +} + +int _glfwPlatformGetIMEStatus(_GLFWwindow* window) +{ + TISInputSourceRef currentSource = TISCopyCurrentKeyboardInputSource(); + NSString* currentSourceID = (__bridge NSString *)(TISGetInputSourceProperty(currentSource, kTISPropertyInputSourceID)); + NSArray* asciiInputSources = CFBridgingRelease(TISCreateASCIICapableInputSourceList()); + TISInputSourceRef asciiSource = (__bridge TISInputSourceRef)([asciiInputSources firstObject]); + if (asciiSource) { + NSString* asciiSourceID = (__bridge NSString *)(TISGetInputSourceProperty(asciiSource, kTISPropertyInputSourceID)); + return ([asciiSourceID compare: currentSourceID] == NSOrderedSame) ? GLFW_FALSE : GLFW_TRUE; + } + return GLFW_FALSE; +} ////////////////////////////////////////////////////////////////////////// ////// GLFW native API ////// diff --git a/src/input.c b/src/input.c index f44ea575..dd8dfac1 100644 --- a/src/input.c +++ b/src/input.c @@ -79,6 +79,20 @@ void _glfwInputChar(_GLFWwindow* window, unsigned int codepoint, int mods, GLFWb } } +void _glfwInputPreedit(_GLFWwindow* window, int focusedBlock) +{ + if (window->callbacks.preedit) { + window->callbacks.preedit((GLFWwindow*) window, window->ntext, window->preeditText, window->nblocks, window->preeditAttributeBlocks, focusedBlock); + } +} + +void _glfwInputIMEStatus(_GLFWwindow* window) +{ + if (window->callbacks.imestatus) { + window->callbacks.imestatus((GLFWwindow*) window); + } +} + void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset) { if (window->callbacks.scroll) @@ -162,6 +176,8 @@ GLFWAPI int glfwGetInputMode(GLFWwindow* handle, int mode) return window->stickyKeys; case GLFW_STICKY_MOUSE_BUTTONS: return window->stickyMouseButtons; + case GLFW_IME: + return _glfwPlatformGetIMEStatus(window); default: _glfwInputError(GLFW_INVALID_ENUM, "Invalid input mode %i", mode); return 0; @@ -245,6 +261,13 @@ GLFWAPI void glfwSetInputMode(GLFWwindow* handle, int mode, int value) window->stickyMouseButtons = value ? GLFW_TRUE : GLFW_FALSE; return; } + + case GLFW_IME: + _glfwPlatformSetIMEStatus(window, value ? GLFW_TRUE : GLFW_FALSE); + break; + default: + _glfwInputError(GLFW_INVALID_ENUM, "Invalid input mode %i", mode); + break; } _glfwInputError(GLFW_INVALID_ENUM, "Invalid input mode %i", mode); @@ -469,6 +492,30 @@ GLFWAPI void glfwSetCursor(GLFWwindow* windowHandle, GLFWcursor* cursorHandle) _glfwPlatformSetCursor(window, cursor); } +GLFWAPI void glfwGetPreeditCursorPos(GLFWwindow* handle, int *x, int *y, int *h) +{ + _GLFWwindow* window = (_GLFWwindow*) handle; + if (x) + *x = window->preeditCursorPosX; + if (y) + *y = window->preeditCursorPosY; + if (h) + *h = window->preeditCursorHeight; +} + +GLFWAPI void glfwSetPreeditCursorPos(GLFWwindow* handle, int x, int y, int h) +{ + _GLFWwindow* window = (_GLFWwindow*) handle; + window->preeditCursorPosX = x; + window->preeditCursorPosY = y; + window->preeditCursorHeight = h; +} + +GLFWAPI void glfwResetPreeditText(GLFWwindow* handle) { + _GLFWwindow* window = (_GLFWwindow*) handle; + _glfwPlatformResetPreeditText(window); +} + GLFWAPI GLFWkeyfun glfwSetKeyCallback(GLFWwindow* handle, GLFWkeyfun cbfun) { _GLFWwindow* window = (_GLFWwindow*) handle; @@ -499,6 +546,22 @@ GLFWAPI GLFWcharmodsfun glfwSetCharModsCallback(GLFWwindow* handle, GLFWcharmods return cbfun; } +GLFWAPI GLFWpreeditfun glfwSetPreeditCallback(GLFWwindow* handle, GLFWpreeditfun cbfun) +{ + _GLFWwindow* window = (_GLFWwindow*) handle; + _GLFW_REQUIRE_INIT_OR_RETURN(NULL); + _GLFW_SWAP_POINTERS(window->callbacks.preedit, cbfun); + return cbfun; +} + +GLFWAPI GLFWimestatusfun glfwSetIMEStatusCallback(GLFWwindow* handle, GLFWimestatusfun cbfun) +{ + _GLFWwindow* window = (_GLFWwindow*) handle; + _GLFW_REQUIRE_INIT_OR_RETURN(NULL); + _GLFW_SWAP_POINTERS(window->callbacks.imestatus, cbfun); + return cbfun; +} + GLFWAPI GLFWmousebuttonfun glfwSetMouseButtonCallback(GLFWwindow* handle, GLFWmousebuttonfun cbfun) { diff --git a/src/internal.h b/src/internal.h index 42e8d539..5052cf1c 100644 --- a/src/internal.h +++ b/src/internal.h @@ -370,9 +370,19 @@ struct _GLFWwindow int cursorMode; char mouseButtons[GLFW_MOUSE_BUTTON_LAST + 1]; char keys[GLFW_KEY_LAST + 1]; + // Virtual cursor position when cursor is disabled double virtualCursorPosX, virtualCursorPosY; + // Preedit texts + unsigned int* preeditText; + int ntext; + int ctext; + int* preeditAttributeBlocks; + int nblocks; + int cblocks; + int preeditCursorPosX, preeditCursorPosY, preeditCursorHeight; + _GLFWcontext context; struct { @@ -391,6 +401,8 @@ struct _GLFWwindow GLFWkeyfun key; GLFWcharfun character; GLFWcharmodsfun charmods; + GLFWpreeditfun preedit; + GLFWimestatusfun imestatus; GLFWdropfun drop; } callbacks; @@ -818,6 +830,46 @@ int _glfwPlatformGetPhysicalDevicePresentationSupport(VkInstance instance, VkPhy */ VkResult _glfwPlatformCreateWindowSurface(VkInstance instance, _GLFWwindow* window, const VkAllocationCallbacks* allocator, VkSurfaceKHR* surface); +/*! @copydoc glfwResetPreeditText + * @ingroup platform + */ +void _glfwPlatformResetPreeditText(_GLFWwindow* window); + +/*! @brief Set IME status. + * + * This function set IME status. + * + * @param[in] window The window. + * @param[in] active Turns on IME if `GFLW_TRUE` is given. Otherwise (`GLFW_FALSE`) turns off. + * + * @par Thread Safety + * This function may only be called from the main thread. + * + * @sa @ref preedit + * + * @since Added in GLFW 3.X. + * + * @ingroup platform + */ +void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active); + +/*! @brief Get IME status. + * + * This function get IME status. + * + * @param[in] window The window. + * @return When IME is active, this function returns `GFLW_TRUE`. Otherwise `GLFW_FALSE`. + * + * @par Thread Safety + * This function may only be called from the main thread. + * + * @sa @ref preedit + * + * @since Added in GLFW 3.X. + * + * @ingroup platform + */ +int _glfwPlatformGetIMEStatus(_GLFWwindow* window); //======================================================================== // Event API functions @@ -904,6 +956,19 @@ void _glfwInputKey(_GLFWwindow* window, int key, int scancode, int action, int m */ void _glfwInputChar(_GLFWwindow* window, unsigned int codepoint, int mods, GLFWbool plain); +/*! @brief Notifies shared code of a IME preedit text update event. + * @param[in] window The window that received the event. + * @param[in] focusedBlock Focused preedit text block index. + * @ingroup event + */ +void _glfwInputPreedit(_GLFWwindow* window, int focusedBlock); + +/*! @brief Notifies shared code of a IME status change. + * @param[in] window The window that received the event. + * @ingroup event + */ +void _glfwInputIMEStatus(_GLFWwindow* window); + /*! @brief Notifies shared code of a scroll event. * @param[in] window The window that received the event. * @param[in] xoffset The scroll offset along the x-axis. diff --git a/src/mir_window.c b/src/mir_window.c index 7d2c276a..6ed6d2c6 100644 --- a/src/mir_window.c +++ b/src/mir_window.c @@ -890,6 +890,25 @@ VkResult _glfwPlatformCreateWindowSurface(VkInstance instance, return err; } +void _glfwPlatformResetPreeditText(_GLFWwindow* window) +{ + _glfwInputError(GLFW_PLATFORM_ERROR, + "Mir: Unsupported function %s", __PRETTY_FUNCTION__); +} + +void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active) +{ + _glfwInputError(GLFW_PLATFORM_ERROR, + "Mir: Unsupported function %s", __PRETTY_FUNCTION__); +} + +int _glfwPlatformGetIMEStatus(_GLFWwindow* window) +{ + _glfwInputError(GLFW_PLATFORM_ERROR, + "Mir: Unsupported function %s", __PRETTY_FUNCTION__); + return GLFW_FALSE; +} + ////////////////////////////////////////////////////////////////////////// ////// GLFW native API ////// diff --git a/src/osmesa_window.c b/src/osmesa_window.c index cba95530..a435077b 100644 --- a/src/osmesa_window.c +++ b/src/osmesa_window.c @@ -301,3 +301,16 @@ VkResult _glfwPlatformCreateWindowSurface(VkInstance instance, return VK_ERROR_INITIALIZATION_FAILED; } +void _glfwPlatformResetPreeditText(_GLFWwindow* window) +{ +} + +void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active) +{ +} + +int _glfwPlatformGetIMEStatus(_GLFWwindow* window) +{ + return GLFW_FALSE; +} + diff --git a/src/win32_window.c b/src/win32_window.c index 9214a66c..6b7cc568 100644 --- a/src/win32_window.c +++ b/src/win32_window.c @@ -33,6 +33,7 @@ #include #include #include +#include #define _GLFW_KEY_INVALID -2 @@ -422,6 +423,15 @@ static void releaseMonitor(_GLFWwindow* window) _glfwRestoreVideoModeWin32(window->monitor); } +// Set cursor position to decide candidate window +static void _win32ChangeCursorPosition(HIMC hIMC, _GLFWwindow* window) { + int x = window->preeditCursorPosX; + int y = window->preeditCursorPosY; + int h = window->preeditCursorHeight; + CANDIDATEFORM excludeRect = {0, CFS_EXCLUDE, {x, y}, {x, y, x, y+h}}; + ImmSetCandidateWindow(hIMC, &excludeRect); +} + // Window callback function (handles window messages) // static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, @@ -536,7 +546,7 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, } _glfwInputChar(window, (unsigned int) wParam, getKeyMods(), plain); - return 0; + return TRUE; } case WM_KEYDOWN: @@ -570,7 +580,93 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, break; } - + case WM_IME_COMPOSITION: + { + if (lParam & GCS_RESULTSTR) { + window->nblocks = 0; + window->ntext = 0; + _glfwInputPreedit(window, 0); + return TRUE; + } + if (lParam & GCS_COMPSTR) { + HIMC hIMC = ImmGetContext(hWnd); + // get preedit data sizes + LONG preeditTextLength = ImmGetCompositionStringW(hIMC, GCS_COMPSTR, NULL, 0); + LONG attrLength = ImmGetCompositionString(hIMC, GCS_COMPATTR, NULL, 0); + LONG clauseLength = ImmGetCompositionString(hIMC, GCS_COMPCLAUSE, NULL, 0); + if (preeditTextLength > 0) { + // get preedit data + int length = preeditTextLength/sizeof(WCHAR); + LPWSTR buffer = (LPWSTR)malloc(sizeof(WCHAR)+preeditTextLength); + LPSTR attributes = (LPSTR)malloc(attrLength); + DWORD *clauses = (DWORD*)malloc(clauseLength); + ImmGetCompositionStringW(hIMC, GCS_COMPSTR, buffer, preeditTextLength); + ImmGetCompositionString(hIMC, GCS_COMPATTR, attributes, attrLength); + ImmGetCompositionString(hIMC, GCS_COMPCLAUSE, clauses, clauseLength); + // store preedit text + int ctext = window->ctext; + while (ctext < length+1) { + ctext = (ctext == 0) ? 1 : ctext*2; + } + if (ctext != window->ctext) { + unsigned int* preeditText = realloc(window->preeditText, sizeof(unsigned int)*ctext); + if (preeditText == NULL) { + return 0; + free(buffer); + free(attributes); + free(clauses); + } + window->preeditText = preeditText; + window->ctext = ctext; + } + window->ntext = length; + window->preeditText[length] = 0; + int i; + for (i=0; i < length; i++) { + window->preeditText[i] = buffer[i]; + } + // store blocks + window->nblocks = clauseLength/sizeof(DWORD)-1; + // last element of clauses is a block count, but + // text length is convenient. + clauses[window->nblocks] = length; + int cblocks = window->cblocks; + while (cblocks < window->nblocks) { + cblocks = (cblocks == 0) ? 1 : cblocks*2; + } + if (cblocks != window->cblocks) { + int* blocks = realloc(window->preeditAttributeBlocks, sizeof(int)*cblocks); + if (blocks == NULL) { + return 0; + free(buffer); + free(attributes); + free(clauses); + } + window->preeditAttributeBlocks = blocks; + window->cblocks = cblocks; + } + int focusedBlock = 0; + for (i=0; i < window->nblocks; i++) { + window->preeditAttributeBlocks[i] = clauses[i+1]-clauses[i]; + if (attributes[clauses[i]] != ATTR_CONVERTED) { + focusedBlock = i; + } + } + free(buffer); + free(attributes); + free(clauses); + _glfwInputPreedit(window, focusedBlock); + _win32ChangeCursorPosition(hIMC, window); + } + ImmReleaseContext(hWnd, hIMC); + return TRUE; + } + break; + } + case WM_IME_NOTIFY: + if (wParam == IMN_SETOPENSTATUS) + _glfwInputIMEStatus(window); + break; case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MBUTTONDOWN: @@ -1705,6 +1801,30 @@ VkResult _glfwPlatformCreateWindowSurface(VkInstance instance, return err; } +void _glfwPlatformResetPreeditText(_GLFWwindow* window) +{ + HWND hWnd = window->win32.handle; + HIMC hIMC = ImmGetContext(hWnd); + ImmNotifyIME(hIMC, NI_COMPOSITIONSTR, CPS_CANCEL, 0); + ImmReleaseContext(hWnd, hIMC); +} + +void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active) +{ + HWND hWnd = window->win32.handle; + HIMC hIMC = ImmGetContext(hWnd); + ImmSetOpenStatus(hIMC, active ? TRUE : FALSE); + ImmReleaseContext(hWnd, hIMC); +} + +int _glfwPlatformGetIMEStatus(_GLFWwindow* window) +{ + HWND hWnd = window->win32.handle; + HIMC hIMC = ImmGetContext(hWnd); + BOOL result = ImmGetOpenStatus(hIMC); + ImmReleaseContext(hWnd, hIMC); + return result ? GLFW_TRUE : GLFW_FALSE; +} ////////////////////////////////////////////////////////////////////////// ////// GLFW native API ////// diff --git a/src/window.c b/src/window.c index ece8d75b..75b92764 100644 --- a/src/window.c +++ b/src/window.c @@ -230,6 +230,10 @@ GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height, if (wndconfig.focused) _glfwPlatformFocusWindow(window); } + + window->preeditCursorPosX = 0; + window->preeditCursorPosY = height; + window->preeditCursorHeight = 0; } return (GLFWwindow*) window; @@ -408,7 +412,11 @@ GLFWAPI void glfwDestroyWindow(GLFWwindow* handle) *prev = window->next; } - + // Clear memory for preedit text + if (window->preeditText) + free(window->preeditText); + if (window->preeditAttributeBlocks) + free(window->preeditAttributeBlocks); free(window); } diff --git a/src/wl_window.c b/src/wl_window.c index a2bae373..88770477 100644 --- a/src/wl_window.c +++ b/src/wl_window.c @@ -1027,6 +1027,28 @@ VkResult _glfwPlatformCreateWindowSurface(VkInstance instance, return err; } +void _glfwPlatformResetPreeditText(_GLFWwindow* window) +{ + // TODO + _glfwInputError(GLFW_PLATFORM_ERROR, + "Wayland: IME not implemented yet"); +} + +void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active) +{ + // TODO + _glfwInputError(GLFW_PLATFORM_ERROR, + "Wayland: IME not implemented yet"); +} + +int _glfwPlatformGetIMEStatus(_GLFWwindow* window) +{ + // TODO + _glfwInputError(GLFW_PLATFORM_ERROR, + "Wayland: IME not implemented yet"); + return GLFW_FALSE; +} + ////////////////////////////////////////////////////////////////////////// ////// GLFW native API ////// diff --git a/src/x11_platform.h b/src/x11_platform.h index 4268f262..6f79e197 100644 --- a/src/x11_platform.h +++ b/src/x11_platform.h @@ -128,6 +128,16 @@ typedef struct _GLFWwindowX11 unsigned int lastKeyCode; Time lastKeyTime; + // Preedit callbacks + XIMCallback preeditStartCallback; + XIMCallback preeditDoneCallback; + XIMCallback preeditDrawCallback; + XIMCallback preeditCaretCallback; + XIMCallback statusStartCallback; + XIMCallback statusDoneCallback; + XIMCallback statusDrawCallback; + + int imeFocus; } _GLFWwindowX11; // X11-specific global data diff --git a/src/x11_window.c b/src/x11_window.c index d552e269..0fcc3aa8 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -48,6 +48,9 @@ #define Button6 6 #define Button7 7 +#if defined(X_HAVE_UTF8_STRING) +static unsigned int decodeUTF8(const char** s); +#endif // Wait for data to arrive using select // This avoids blocking other threads via the per-display Xlib lock that also @@ -437,6 +440,196 @@ static char** parseUriList(char* text, int* count) return paths; } +// Update cursor position to decide candidate window +static void _ximChangeCursorPosition(XIC xic, _GLFWwindow* window) +{ + XVaNestedList preedit_attr; + XPoint spot; + + spot.x = window->preeditCursorPosX; + spot.y = window->preeditCursorPosY + window->preeditCursorHeight; + preedit_attr = XVaCreateNestedList(0, XNSpotLocation, &spot, NULL); + XSetICValues(xic, XNPreeditAttributes, preedit_attr, NULL); + XFree(preedit_attr); +} + +// IME Start callback (do nothing) +static void _ximPreeditStartCallback(XIC xic, XPointer clientData, XPointer callData) +{ +} + +// IME Done callback (do nothing) +static void _ximPreeditDoneCallback(XIC xic, XPointer clientData, XPointer callData) +{ +} + +// IME Draw callback +static void _ximPreeditDrawCallback(XIC xic, XPointer clientData, XIMPreeditDrawCallbackStruct *callData) +{ + int i, j, length, ctext, rstart, rend; + XIMText* text; + const char* src; + unsigned int codePoint; + unsigned int* preeditText; + XIMFeedback f; + _GLFWwindow* window = (_GLFWwindow*)clientData; + + // keep cursor position to reduce API call + int cursorX = window->preeditCursorPosX; + int cursorY = window->preeditCursorPosY; + int cursorHeight = window->preeditCursorHeight; + + if (!callData->text) { + // preedit text is empty + window->ntext = 0; + window->nblocks = 0; + _glfwInputPreedit(window, 0); + return; + } else { + text = callData->text; + length = callData->chg_length; + if (text->encoding_is_wchar) { + // wchar is not supported + return; + } + ctext = window->ctext; + while (ctext < length+1) { + ctext = (ctext == 0) ? 1 : ctext * 2; + } + if (ctext != window->ctext) { + preeditText = realloc(window->preeditText, sizeof(unsigned int)*ctext); + if (preeditText == NULL) { + return; + } + window->preeditText = preeditText; + window->ctext = ctext; + } + window->ntext = length; + window->preeditText[length] = 0; + if (window->cblocks == 0) { + window->preeditAttributeBlocks = malloc(sizeof(int)*4); + window->cblocks = 4; + } + src = text->string.multi_byte; + rend = 0; + rstart = length; + for (i = 0, j = 0; i < text->length; i++) { + #if defined(X_HAVE_UTF8_STRING) + codePoint = decodeUTF8(&src); + #else + codePoint = *src; + src++; + #endif + if (i < callData->chg_first || callData->chg_first+length < i) { + continue; + } + window->preeditText[j++] = codePoint; + f = text->feedback[i]; + if ((f & XIMReverse) || (f & XIMHighlight)) { + rend = i; + if (i < rstart) { + rstart = i; + } + } + } + if (rstart == length) { + window->nblocks = 1; + window->preeditAttributeBlocks[0] = length; + window->preeditAttributeBlocks[1] = 0; + _glfwInputPreedit(window, 0); + } else if (rstart == 0) { + if (rend == length -1) { + window->nblocks = 1; + window->preeditAttributeBlocks[0] = length; + window->preeditAttributeBlocks[1] = 0; + _glfwInputPreedit(window, 0); + } else { + window->nblocks = 2; + window->preeditAttributeBlocks[0] = rend + 1; + window->preeditAttributeBlocks[1] = length - rend - 1; + window->preeditAttributeBlocks[2] = 0; + _glfwInputPreedit(window, 0); + } + } else if (rend == length -1) { + window->nblocks = 2; + window->preeditAttributeBlocks[0] = rstart; + window->preeditAttributeBlocks[1] = length - rstart; + window->preeditAttributeBlocks[2] = 0; + _glfwInputPreedit(window, 1); + } else { + window->nblocks = 3; + window->preeditAttributeBlocks[0] = rstart; + window->preeditAttributeBlocks[1] = rend - rstart + 1; + window->preeditAttributeBlocks[2] = length - rend - 1; + window->preeditAttributeBlocks[3] = 0; + _glfwInputPreedit(window, 1); + } + if ((cursorX != window->preeditCursorPosX) + || (cursorY != window->preeditCursorPosY) + || (cursorHeight != window->preeditCursorHeight)) { + _ximChangeCursorPosition(xic, window); + } + } +} + +// IME Caret callback (do nothing) +static void _ximPreeditCaretCallback(XIC xic, XPointer clientData, XPointer callData) +{ +} + +static void _ximStatusStartCallback(XIC xic, XPointer clientData, XPointer callData) +{ + _GLFWwindow* window = (_GLFWwindow*)clientData; + window->x11.imeFocus = GLFW_TRUE; +} + +static void _ximStatusDoneCallback(XIC xic, XPointer clientData, XPointer callData) +{ + _GLFWwindow* window = (_GLFWwindow*)clientData; + window->x11.imeFocus = GLFW_FALSE; +} + +static void _ximStatusDrawCallback(XIC xic, XPointer clientData, XIMStatusDrawCallbackStruct* callData) +{ + _GLFWwindow* window = (_GLFWwindow*)clientData; + _glfwInputIMEStatus(window); +} + +// Create XIM Preedit callback +static XVaNestedList _createXIMPreeditCallbacks(_GLFWwindow* window) +{ + window->x11.preeditStartCallback.client_data = (XPointer)window; + window->x11.preeditStartCallback.callback = (XIMProc)_ximPreeditStartCallback; + window->x11.preeditDoneCallback.client_data = (XPointer)window; + window->x11.preeditDoneCallback.callback = (XIMProc)_ximPreeditDoneCallback; + window->x11.preeditDrawCallback.client_data = (XPointer)window; + window->x11.preeditDrawCallback.callback = (XIMProc)_ximPreeditDrawCallback; + window->x11.preeditCaretCallback.client_data = (XPointer)window; + window->x11.preeditCaretCallback.callback = (XIMProc)_ximPreeditCaretCallback; + return XVaCreateNestedList (0, + XNPreeditStartCallback, &window->x11.preeditStartCallback.client_data, + XNPreeditDoneCallback, &window->x11.preeditDoneCallback.client_data, + XNPreeditDrawCallback, &window->x11.preeditDrawCallback.client_data, + XNPreeditCaretCallback, &window->x11.preeditCaretCallback.client_data, + NULL); +} + +// Create XIM status callback +static XVaNestedList _createXIMStatusCallbacks(_GLFWwindow* window) +{ + window->x11.statusStartCallback.client_data = (XPointer)window; + window->x11.statusStartCallback.callback = (XIMProc)_ximStatusStartCallback; + window->x11.statusDoneCallback.client_data = (XPointer)window; + window->x11.statusDoneCallback.callback = (XIMProc)_ximStatusDoneCallback; + window->x11.statusDrawCallback.client_data = (XPointer)window; + window->x11.statusDrawCallback.callback = (XIMProc)_ximStatusDrawCallback; + return XVaCreateNestedList (0, + XNStatusStartCallback, &window->x11.statusStartCallback.client_data, + XNStatusDoneCallback, &window->x11.statusDoneCallback.client_data, + XNStatusDrawCallback, &window->x11.statusDrawCallback.client_data, + NULL); +} + // Centers the cursor over the window client area // static void centerCursor(_GLFWwindow* window) @@ -646,14 +839,23 @@ static GLFWbool createNativeWindow(_GLFWwindow* window, if (_glfw.x11.im) { + XVaNestedList preeditList = _createXIMPreeditCallbacks(window); + XVaNestedList statusList = _createXIMStatusCallbacks(window); window->x11.ic = XCreateIC(_glfw.x11.im, XNInputStyle, - XIMPreeditNothing | XIMStatusNothing, + XIMPreeditCallbacks | XIMStatusCallbacks, XNClientWindow, window->x11.handle, XNFocusWindow, window->x11.handle, + XNPreeditAttributes, + preeditList, + XNStatusAttributes, + statusList, NULL); + XFree(preeditList); + XFree(statusList); + window->x11.imeFocus = GLFW_FALSE; } _glfwPlatformGetWindowPos(window, &window->x11.xpos, &window->x11.ypos); @@ -2480,6 +2682,46 @@ VkResult _glfwPlatformCreateWindowSurface(VkInstance instance, } } +void _glfwPlatformResetPreeditText(_GLFWwindow* window) { + XIC ic = window->x11.ic; + + /* restore conversion state after resetting ic later */ + XIMPreeditState preedit_state = XIMPreeditUnKnown; + XVaNestedList preedit_attr; + char* result; + + if (window->ntext == 0) + return; + + preedit_attr = XVaCreateNestedList(0, XNPreeditState, &preedit_state, NULL); + XGetICValues(ic, XNPreeditAttributes, preedit_attr, NULL); + XFree(preedit_attr); + + result = XmbResetIC(ic); + + preedit_attr = XVaCreateNestedList(0, XNPreeditState, preedit_state, NULL); + XSetICValues(ic, XNPreeditAttributes, preedit_attr, NULL); + XFree(preedit_attr); + + window->ntext = 0; + window->nblocks = 0; + _glfwInputPreedit(window, 0); + + XFree (result); +} + +void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active) { + XIC ic = window->x11.ic; + if (active) { + XSetICFocus(ic); + } else { + XUnsetICFocus(ic); + } +} + +int _glfwPlatformGetIMEStatus(_GLFWwindow* window) { + return window->x11.imeFocus; +} ////////////////////////////////////////////////////////////////////////// ////// GLFW native API ////// diff --git a/tests/events.c b/tests/events.c index 73a61ab2..5915078f 100644 --- a/tests/events.c +++ b/tests/events.c @@ -420,6 +420,45 @@ static void char_mods_callback(GLFWwindow* window, unsigned int codepoint, int m get_mods_name(mods)); } +static void preedit_callback(GLFWwindow* window, int strLength, unsigned int* string, int blockLength, int* blocks, int focusedBlock) { + Slot* slot = glfwGetWindowUserPointer(window); + int i, blockIndex = -1, blockCount = 0; + int width, height; + printf("%08x to %i at %0.3f: Preedit text ", + counter++, slot->number, glfwGetTime()); + if (strLength == 0 || blockLength == 0) { + printf("(empty)\n"); + } else { + for (i = 0; i < strLength; i++) { + if (blockCount == 0) { + if (blockIndex == focusedBlock) { + printf("]"); + } + blockIndex++; + blockCount = blocks[blockIndex]; + printf("\n block %d: ", blockIndex); + if (blockIndex == focusedBlock) { + printf("["); + } + } + printf("%s", get_character_string(string[i])); + blockCount--; + } + if (blockIndex == focusedBlock) { + printf("]"); + } + printf("\n"); + glfwGetWindowSize(window, &width, &height); + glfwSetPreeditCursorPos(window, width/2, height/2, 20); + } +} + +static void ime_callback(GLFWwindow* window) { + Slot* slot = glfwGetWindowUserPointer(window); + printf("%08x to %i at %0.3f: IME switched\n", + counter++, slot->number, glfwGetTime()); +} + static void drop_callback(GLFWwindow* window, int count, const char** paths) { int i; @@ -596,6 +635,8 @@ int main(int argc, char** argv) glfwSetKeyCallback(slots[i].window, key_callback); glfwSetCharCallback(slots[i].window, char_callback); glfwSetCharModsCallback(slots[i].window, char_mods_callback); + glfwSetPreeditCallback(slots[i].window, preedit_callback); + glfwSetIMEStatusCallback(slots[i].window, ime_callback); glfwSetDropCallback(slots[i].window, drop_callback); glfwMakeContextCurrent(slots[i].window);