From d36a164423c933948661f3f17576e5a6388ff251 Mon Sep 17 00:00:00 2001 From: Yoshiki Shibukawa Date: Thu, 14 Mar 2019 02:03:46 +0900 Subject: [PATCH] fixup ef70fee8 Add preedit text callback API and implementation, IME status API for X11, macOS, Windows --- .gitignore | 2 +- CMakeLists.txt | 4 +- docs/input.dox | 90 ++++++++ include/GLFW/glfw3.h | 164 +++++++++++++- src/cocoa_window.m | 105 ++++++++- src/input.c | 62 +++++- src/internal.h | 16 ++ src/win32_window.c | 124 ++++++++++- src/window.c | 12 +- src/x11_platform.h | 10 + src/x11_window.c | 244 ++++++++++++++++++++- tests/CMakeLists.txt | 3 +- tests/events.c | 41 ++++ tests/ime.c | 508 +++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 1367 insertions(+), 18 deletions(-) create mode 100644 tests/ime.c diff --git a/.gitignore b/.gitignore index 859499268..db65a1507 100644 --- a/.gitignore +++ b/.gitignore @@ -82,4 +82,4 @@ tests/timeout tests/title tests/vulkan tests/windows - +tests/ime diff --git a/CMakeLists.txt b/CMakeLists.txt index 414162969..af6bb2b12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -205,6 +205,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) @@ -298,12 +299,13 @@ if (_GLFW_COCOA) list(APPEND glfw_LIBRARIES "-framework Cocoa" + "-framework Carbon" "-framework IOKit" "-framework CoreFoundation" "-framework CoreVideo") set(glfw_PKG_DEPS "") - set(glfw_PKG_LIBS "-framework Cocoa -framework IOKit -framework CoreFoundation -framework CoreVideo") + set(glfw_PKG_LIBS "-framework Cocoa -framework Carbon -framework IOKit -framework CoreFoundation -framework CoreVideo") endif() #-------------------------------------------------------------------- diff --git a/docs/input.dox b/docs/input.dox index a438a18e1..3b567d945 100644 --- a/docs/input.dox +++ b/docs/input.dox @@ -214,6 +214,96 @@ void character_callback(GLFWwindow* window, unsigned int codepoint) } @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 fb0980ab3..4a2119b94 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -1003,6 +1003,7 @@ extern "C" { #define GLFW_STICKY_MOUSE_BUTTONS 0x00033003 #define GLFW_LOCK_KEY_MODS 0x00033004 #define GLFW_RAW_MOUSE_MOTION 0x00033005 +#define GLFW_IME 0x00033006 #define GLFW_CURSOR_NORMAL 0x00034001 #define GLFW_CURSOR_HIDDEN 0x00034002 @@ -1458,6 +1459,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. @@ -3844,13 +3876,13 @@ GLFWAPI void glfwPostEmptyEvent(void); * * This function returns the value of an input option for the specified window. * The mode must be one of @ref GLFW_CURSOR, @ref GLFW_STICKY_KEYS, - * @ref GLFW_STICKY_MOUSE_BUTTONS, @ref GLFW_LOCK_KEY_MODS or - * @ref GLFW_RAW_MOUSE_MOTION. + * @ref GLFW_STICKY_MOUSE_BUTTONS, @ref GLFW_LOCK_KEY_MODS, + * @ref GLFW_RAW_MOUSE_MOTION or @ref GLFW_IME. * * @param[in] window The window to query. * @param[in] mode One of `GLFW_CURSOR`, `GLFW_STICKY_KEYS`, - * `GLFW_STICKY_MOUSE_BUTTONS`, `GLFW_LOCK_KEY_MODS` or - * `GLFW_RAW_MOUSE_MOTION`. + * `GLFW_STICKY_MOUSE_BUTTONS`, `GLFW_LOCK_KEY_MODS` + * `GLFW_RAW_MOUSE_MOTION` or `GLFW_IME`. * * @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref * GLFW_INVALID_ENUM. @@ -3869,8 +3901,8 @@ GLFWAPI int glfwGetInputMode(GLFWwindow* window, int mode); * * This function sets an input mode option for the specified window. The mode * must be one of @ref GLFW_CURSOR, @ref GLFW_STICKY_KEYS, - * @ref GLFW_STICKY_MOUSE_BUTTONS, @ref GLFW_LOCK_KEY_MODS or - * @ref GLFW_RAW_MOUSE_MOTION. + * @ref GLFW_STICKY_MOUSE_BUTTONS, @ref GLFW_LOCK_KEY_MODS, + * @ref GLFW_RAW_MOUSE_MOTION or @ref GLFW_IME. * * If the mode is `GLFW_CURSOR`, the value must be one of the following cursor * modes: @@ -3908,10 +3940,13 @@ GLFWAPI int glfwGetInputMode(GLFWwindow* window, int mode); * attempting to set this will emit @ref GLFW_PLATFORM_ERROR. Call @ref * glfwRawMouseMotionSupported to check for support. * + * 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`, - * `GLFW_STICKY_MOUSE_BUTTONS`, `GLFW_LOCK_KEY_MODS` or - * `GLFW_RAW_MOUSE_MOTION`. + * `GLFW_STICKY_MOUSE_BUTTONS`, `GLFW_LOCK_KEY_MODS`, + * `GLFW_RAW_MOUSE_MOTION` or `GLFW_IME`. * @param[in] value The new value of the specified input mode. * * @errors Possible errors include @ref GLFW_NOT_INITIALIZED, @ref @@ -4308,6 +4343,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 @@ -4422,6 +4518,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 888edc9a6..813a12b17 100644 --- a/src/cocoa_window.m +++ b/src/cocoa_window.m @@ -344,8 +344,11 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; _glfwUpdateDisplayLinkDisplayNSGL(window); } -@end +- (void)imeStatusChangeNotified:(NSNotification *)notification { + _glfwInputIMEStatus(window); +} +@end //------------------------------------------------------------------------ // Content view class for the GLFW window @@ -712,6 +715,56 @@ static const NSRange kEmptyRange = { NSNotFound, 0 }; markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string]; else markedText = [[NSMutableAttributedString alloc] 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 @@ -959,6 +1012,11 @@ int _glfwPlatformCreateWindow(_GLFWwindow* window, acquireMonitor(window); } + [[NSNotificationCenter defaultCenter] + addObserver: window->ns.delegate + selector:@selector(imeStatusChangeNotified:) + name:NSTextInputContextKeyboardSelectionDidChangeNotification + object: nil]; return GLFW_TRUE; } // autoreleasepool @@ -971,6 +1029,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) @@ -1776,6 +1836,49 @@ VkResult _glfwPlatformCreateWindowSurface(VkInstance instance, } // autoreleasepool } +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 7ee428117..7236f4c00 100644 --- a/src/input.c +++ b/src/input.c @@ -305,6 +305,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); + } +} + // Notifies shared code of a scroll event // void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset) @@ -487,7 +501,8 @@ GLFWAPI int glfwGetInputMode(GLFWwindow* handle, int mode) return window->lockKeyMods; case GLFW_RAW_MOUSE_MOTION: return window->rawMouseMotion; - } + case GLFW_IME: + return _glfwPlatformGetIMEStatus(window); } _glfwInputError(GLFW_INVALID_ENUM, "Invalid input mode 0x%08X", mode); return 0; @@ -582,7 +597,10 @@ GLFWAPI void glfwSetInputMode(GLFWwindow* handle, int mode, int value) window->rawMouseMotion = value; _glfwPlatformSetRawMouseMotion(window, value); } - else + else if (mode == GLFW_IME) + { + _glfwPlatformSetIMEStatus(window, value ? GLFW_TRUE : GLFW_FALSE); + } else _glfwInputError(GLFW_INVALID_ENUM, "Invalid input mode 0x%08X", mode); } @@ -824,6 +842,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; @@ -854,6 +896,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 3d5e22f7e..50818d404 100644 --- a/src/internal.h +++ b/src/internal.h @@ -392,6 +392,15 @@ struct _GLFWwindow double virtualCursorPosX, virtualCursorPosY; GLFWbool rawMouseMotion; + // Preedit texts + unsigned int* preeditText; + int ntext; + int ctext; + int* preeditAttributeBlocks; + int nblocks; + int cblocks; + int preeditCursorPosX, preeditCursorPosY, preeditCursorHeight; + _GLFWcontext context; struct { @@ -411,6 +420,8 @@ struct _GLFWwindow GLFWkeyfun key; GLFWcharfun character; GLFWcharmodsfun charmods; + GLFWpreeditfun preedit; + GLFWimestatusfun imestatus; GLFWdropfun drop; } callbacks; @@ -716,6 +727,8 @@ void _glfwInputKey(_GLFWwindow* window, int key, int scancode, int action, int mods); void _glfwInputChar(_GLFWwindow* window, unsigned int codepoint, int mods, GLFWbool plain); +void _glfwInputPreedit(_GLFWwindow* window, int focusedBlock); +void _glfwInputIMEStatus(_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); @@ -736,6 +749,9 @@ void _glfwInputError(int code, const char* format, ...) void _glfwInputError(int code, const char* format, ...); #endif +void _glfwPlatformResetPreeditText(_GLFWwindow* window); +void _glfwPlatformSetIMEStatus(_GLFWwindow* window, int active); +int _glfwPlatformGetIMEStatus(_GLFWwindow* window); ////////////////////////////////////////////////////////////////////////// ////// GLFW internal API ////// diff --git a/src/win32_window.c b/src/win32_window.c index 728b6e07a..73dad8230 100644 --- a/src/win32_window.c +++ b/src/win32_window.c @@ -33,6 +33,7 @@ #include #include #include +#include #define _GLFW_KEY_INVALID -2 @@ -574,6 +575,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, @@ -733,7 +743,7 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, } _glfwInputChar(window, (unsigned int) wParam, getKeyMods(), plain); - return 0; + return TRUE; } case WM_KEYDOWN: @@ -768,7 +778,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: @@ -2229,6 +2325,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 91c323fbd..14ed39a78 100644 --- a/src/window.c +++ b/src/window.c @@ -230,6 +230,9 @@ GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height, { if (wndconfig.centerCursor) _glfwCenterCursorInContentArea(window); + window->preeditCursorPosX = 0; + window->preeditCursorPosY = height; + window->preeditCursorHeight = 0; } else { @@ -239,6 +242,9 @@ GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height, if (wndconfig.focused) _glfwPlatformFocusWindow(window); } + window->preeditCursorPosX = 0; + window->preeditCursorPosY = height; + window->preeditCursorHeight = 0; } return (GLFWwindow*) window; @@ -465,7 +471,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/x11_platform.h b/src/x11_platform.h index f9e7194ce..985c1ace2 100644 --- a/src/x11_platform.h +++ b/src/x11_platform.h @@ -201,6 +201,16 @@ typedef struct _GLFWwindowX11 // The time of the last KeyPress event 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 92a137f48..76d023e76 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -50,6 +50,9 @@ #define _GLFW_XDND_VERSION 5 +#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 @@ -587,6 +590,196 @@ static void enableCursor(_GLFWwindow* window) updateCursorImage(window); } +// 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); +} + // Create the X11 window (and its colormap) // static GLFWbool createNativeWindow(_GLFWwindow* window, @@ -774,14 +967,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); @@ -3046,6 +3248,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/CMakeLists.txt b/tests/CMakeLists.txt index 8a15da1db..944826427 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,6 +26,7 @@ add_executable(iconify iconify.c ${GETOPT} ${GLAD}) add_executable(monitors monitors.c ${GETOPT} ${GLAD}) add_executable(reopen reopen.c ${GLAD}) add_executable(cursor cursor.c ${GLAD}) +add_executable(ime ime.c ${GETOPT} ${GLAD}) add_executable(empty WIN32 MACOSX_BUNDLE empty.c ${TINYCTHREAD} ${GLAD}) add_executable(gamma WIN32 MACOSX_BUNDLE gamma.c ${GLAD}) @@ -49,7 +50,7 @@ endif() set(WINDOWS_BINARIES empty gamma icon inputlag joysticks opacity tearing threads timeout title windows) set(CONSOLE_BINARIES clipboard events msaa glfwinfo iconify monitors reopen - cursor) + cursor ime) if (VULKAN_FOUND) add_executable(vulkan WIN32 vulkan.c ${ICON}) diff --git a/tests/events.c b/tests/events.c index 5a0fde04a..e47046065 100644 --- a/tests/events.c +++ b/tests/events.c @@ -429,6 +429,45 @@ static void char_callback(GLFWwindow* window, unsigned int codepoint) get_character_string(codepoint)); } +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; @@ -601,6 +640,8 @@ int main(int argc, char** argv) glfwSetScrollCallback(slots[i].window, scroll_callback); glfwSetKeyCallback(slots[i].window, key_callback); glfwSetCharCallback(slots[i].window, char_callback); + glfwSetPreeditCallback(slots[i].window, preedit_callback); + glfwSetIMEStatusCallback(slots[i].window, ime_callback); glfwSetDropCallback(slots[i].window, drop_callback); glfwMakeContextCurrent(slots[i].window); diff --git a/tests/ime.c b/tests/ime.c new file mode 100644 index 000000000..869462414 --- /dev/null +++ b/tests/ime.c @@ -0,0 +1,508 @@ +//======================================================================== +// Event linter (event spewer) +// Copyright (c) Camilla Berglund +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would +// be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not +// be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source +// distribution. +// +//======================================================================== +// +// This test hooks every available callback and outputs their arguments +// +// Log messages go to stdout, error messages to stderr +// +// Every event also gets a (sequential) number to aid discussion of logs +// +//======================================================================== + +#include +#include + +#include +#include +#include +#include +#include + +#include "getopt.h" + +// Event index +static unsigned int counter = 0; + +// Mouse position +static int posX = 0, posY = 0; + +typedef struct +{ + GLFWwindow* window; + int number; + int closeable; +} Slot; + +static void usage(void) +{ + printf("Usage: ime [-f] [-h] [-n WINDOWS]\n"); + printf("Options:\n"); + printf(" -f use full screen\n"); + printf(" -h show this help\n"); + printf(" -n the number of windows to create\n"); +} + +static const char* get_key_name(int key) +{ + switch (key) + { + // Printable keys + case GLFW_KEY_A: return "A"; + case GLFW_KEY_B: return "B"; + case GLFW_KEY_C: return "C"; + case GLFW_KEY_D: return "D"; + case GLFW_KEY_E: return "E"; + case GLFW_KEY_F: return "F"; + case GLFW_KEY_G: return "G"; + case GLFW_KEY_H: return "H"; + case GLFW_KEY_I: return "I"; + case GLFW_KEY_J: return "J"; + case GLFW_KEY_K: return "K"; + case GLFW_KEY_L: return "L"; + case GLFW_KEY_M: return "M"; + case GLFW_KEY_N: return "N"; + case GLFW_KEY_O: return "O"; + case GLFW_KEY_P: return "P"; + case GLFW_KEY_Q: return "Q"; + case GLFW_KEY_R: return "R"; + case GLFW_KEY_S: return "S"; + case GLFW_KEY_T: return "T"; + case GLFW_KEY_U: return "U"; + case GLFW_KEY_V: return "V"; + case GLFW_KEY_W: return "W"; + case GLFW_KEY_X: return "X"; + case GLFW_KEY_Y: return "Y"; + case GLFW_KEY_Z: return "Z"; + case GLFW_KEY_1: return "1"; + case GLFW_KEY_2: return "2"; + case GLFW_KEY_3: return "3"; + case GLFW_KEY_4: return "4"; + case GLFW_KEY_5: return "5"; + case GLFW_KEY_6: return "6"; + case GLFW_KEY_7: return "7"; + case GLFW_KEY_8: return "8"; + case GLFW_KEY_9: return "9"; + case GLFW_KEY_0: return "0"; + case GLFW_KEY_SPACE: return "SPACE"; + case GLFW_KEY_MINUS: return "MINUS"; + case GLFW_KEY_EQUAL: return "EQUAL"; + case GLFW_KEY_LEFT_BRACKET: return "LEFT BRACKET"; + case GLFW_KEY_RIGHT_BRACKET: return "RIGHT BRACKET"; + case GLFW_KEY_BACKSLASH: return "BACKSLASH"; + case GLFW_KEY_SEMICOLON: return "SEMICOLON"; + case GLFW_KEY_APOSTROPHE: return "APOSTROPHE"; + case GLFW_KEY_GRAVE_ACCENT: return "GRAVE ACCENT"; + case GLFW_KEY_COMMA: return "COMMA"; + case GLFW_KEY_PERIOD: return "PERIOD"; + case GLFW_KEY_SLASH: return "SLASH"; + case GLFW_KEY_WORLD_1: return "WORLD 1"; + case GLFW_KEY_WORLD_2: return "WORLD 2"; + + // Function keys + case GLFW_KEY_ESCAPE: return "ESCAPE"; + case GLFW_KEY_F1: return "F1"; + case GLFW_KEY_F2: return "F2"; + case GLFW_KEY_F3: return "F3"; + case GLFW_KEY_F4: return "F4"; + case GLFW_KEY_F5: return "F5"; + case GLFW_KEY_F6: return "F6"; + case GLFW_KEY_F7: return "F7"; + case GLFW_KEY_F8: return "F8"; + case GLFW_KEY_F9: return "F9"; + case GLFW_KEY_F10: return "F10"; + case GLFW_KEY_F11: return "F11"; + case GLFW_KEY_F12: return "F12"; + case GLFW_KEY_F13: return "F13"; + case GLFW_KEY_F14: return "F14"; + case GLFW_KEY_F15: return "F15"; + case GLFW_KEY_F16: return "F16"; + case GLFW_KEY_F17: return "F17"; + case GLFW_KEY_F18: return "F18"; + case GLFW_KEY_F19: return "F19"; + case GLFW_KEY_F20: return "F20"; + case GLFW_KEY_F21: return "F21"; + case GLFW_KEY_F22: return "F22"; + case GLFW_KEY_F23: return "F23"; + case GLFW_KEY_F24: return "F24"; + case GLFW_KEY_F25: return "F25"; + case GLFW_KEY_UP: return "UP"; + case GLFW_KEY_DOWN: return "DOWN"; + case GLFW_KEY_LEFT: return "LEFT"; + case GLFW_KEY_RIGHT: return "RIGHT"; + case GLFW_KEY_LEFT_SHIFT: return "LEFT SHIFT"; + case GLFW_KEY_RIGHT_SHIFT: return "RIGHT SHIFT"; + case GLFW_KEY_LEFT_CONTROL: return "LEFT CONTROL"; + case GLFW_KEY_RIGHT_CONTROL: return "RIGHT CONTROL"; + case GLFW_KEY_LEFT_ALT: return "LEFT ALT"; + case GLFW_KEY_RIGHT_ALT: return "RIGHT ALT"; + case GLFW_KEY_TAB: return "TAB"; + case GLFW_KEY_ENTER: return "ENTER"; + case GLFW_KEY_BACKSPACE: return "BACKSPACE"; + case GLFW_KEY_INSERT: return "INSERT"; + case GLFW_KEY_DELETE: return "DELETE"; + case GLFW_KEY_PAGE_UP: return "PAGE UP"; + case GLFW_KEY_PAGE_DOWN: return "PAGE DOWN"; + case GLFW_KEY_HOME: return "HOME"; + case GLFW_KEY_END: return "END"; + case GLFW_KEY_KP_0: return "KEYPAD 0"; + case GLFW_KEY_KP_1: return "KEYPAD 1"; + case GLFW_KEY_KP_2: return "KEYPAD 2"; + case GLFW_KEY_KP_3: return "KEYPAD 3"; + case GLFW_KEY_KP_4: return "KEYPAD 4"; + case GLFW_KEY_KP_5: return "KEYPAD 5"; + case GLFW_KEY_KP_6: return "KEYPAD 6"; + case GLFW_KEY_KP_7: return "KEYPAD 7"; + case GLFW_KEY_KP_8: return "KEYPAD 8"; + case GLFW_KEY_KP_9: return "KEYPAD 9"; + case GLFW_KEY_KP_DIVIDE: return "KEYPAD DIVIDE"; + case GLFW_KEY_KP_MULTIPLY: return "KEYPAD MULTPLY"; + case GLFW_KEY_KP_SUBTRACT: return "KEYPAD SUBTRACT"; + case GLFW_KEY_KP_ADD: return "KEYPAD ADD"; + case GLFW_KEY_KP_DECIMAL: return "KEYPAD DECIMAL"; + case GLFW_KEY_KP_EQUAL: return "KEYPAD EQUAL"; + case GLFW_KEY_KP_ENTER: return "KEYPAD ENTER"; + case GLFW_KEY_PRINT_SCREEN: return "PRINT SCREEN"; + case GLFW_KEY_NUM_LOCK: return "NUM LOCK"; + case GLFW_KEY_CAPS_LOCK: return "CAPS LOCK"; + case GLFW_KEY_SCROLL_LOCK: return "SCROLL LOCK"; + case GLFW_KEY_PAUSE: return "PAUSE"; + case GLFW_KEY_LEFT_SUPER: return "LEFT SUPER"; + case GLFW_KEY_RIGHT_SUPER: return "RIGHT SUPER"; + case GLFW_KEY_MENU: return "MENU"; + + default: return "UNKNOWN"; + } +} + +static const char* get_action_name(int action) +{ + switch (action) + { + case GLFW_PRESS: + return "pressed"; + case GLFW_RELEASE: + return "released"; + case GLFW_REPEAT: + return "repeated"; + } + + return "caused unknown action"; +} + +static const char* get_mods_name(int mods) +{ + static char name[512]; + + if (mods == 0) + return " no mods"; + + name[0] = '\0'; + + if (mods & GLFW_MOD_SHIFT) + strcat(name, " shift"); + if (mods & GLFW_MOD_CONTROL) + strcat(name, " control"); + if (mods & GLFW_MOD_ALT) + strcat(name, " alt"); + if (mods & GLFW_MOD_SUPER) + strcat(name, " super"); + + return name; +} + +static const char* get_character_string(int codepoint) +{ + // This assumes UTF-8, which is stupid + static char result[6 + 1]; + + int length = wctomb(result, codepoint); + if (length == -1) + length = 0; + + result[length] = '\0'; + return result; +} + +static void error_callback(int error, const char* description) +{ + fprintf(stderr, "Error: %s\n", description); +} + +static void mouse_button_callback(GLFWwindow* window, int button, int action, int mods) +{ + Slot* slot = glfwGetWindowUserPointer(window); + int currentIMEstatus; + int x, y; + if (action == GLFW_PRESS) { + if (button == GLFW_MOUSE_BUTTON_LEFT) { + currentIMEstatus = glfwGetInputMode(window, GLFW_IME); + glfwSetInputMode(window, GLFW_IME, 1-currentIMEstatus); + glfwResetPreeditText(window); + printf("%08x to %i at %0.3f: Reset preedit text and IME status -> %s\n", + counter++, slot->number, glfwGetTime(), currentIMEstatus ? "OFF" : "ON"); + } else if (button == GLFW_MOUSE_BUTTON_RIGHT) { + glfwGetPreeditCursorPos(window, &x, &y, NULL); + glfwSetPreeditCursorPos(window, posX, posY, 20); + printf("%08x to %i at %0.3f: Move preedit text cursor position (%d, %d) -> (%d, %d)\n", + counter++, slot->number, glfwGetTime(), x, y, posX, posY); + } + } +} + +static void cursor_position_callback(GLFWwindow* window, double x, double y) +{ + posX = x; + posY = y; +} + +static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) +{ + Slot* slot = glfwGetWindowUserPointer(window); + const char* name = glfwGetKeyName(key, scancode); + + if (name) + { + printf("%08x to %i at %0.3f: Key 0x%04x Scancode 0x%04x (%s) (%s) (with%s) was %s\n", + counter++, slot->number, glfwGetTime(), key, scancode, + get_key_name(key), + name, + get_mods_name(mods), + get_action_name(action)); + } + else + { + printf("%08x to %i at %0.3f: Key 0x%04x Scancode 0x%04x (%s) (with%s) was %s\n", + counter++, slot->number, glfwGetTime(), key, scancode, + get_key_name(key), + get_mods_name(mods), + get_action_name(action)); + } + + if (action != GLFW_PRESS) + return; + + switch (key) + { + case GLFW_KEY_C: + { + slot->closeable = !slot->closeable; + + printf("(( closing %s ))\n", slot->closeable ? "enabled" : "disabled"); + break; + } + } +} + +static void char_callback(GLFWwindow* window, unsigned int codepoint) +{ + Slot* slot = glfwGetWindowUserPointer(window); + printf("%08x to %i at %0.3f: Character 0x%08x (%s) input\n", + counter++, slot->number, glfwGetTime(), codepoint, + get_character_string(codepoint)); +} + +static void char_mods_callback(GLFWwindow* window, unsigned int codepoint, int mods) +{ + Slot* slot = glfwGetWindowUserPointer(window); + printf("%08x to %i at %0.3f: Character 0x%08x (%s) with modifiers (with%s) input\n", + counter++, slot->number, glfwGetTime(), codepoint, + get_character_string(codepoint), + 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); + } +} + +static void ime_callback(GLFWwindow* window) { + Slot* slot = glfwGetWindowUserPointer(window); + printf("%08x to %i at %0.3f: IME switched\n", + counter++, slot->number, glfwGetTime()); +} + +int main(int argc, char** argv) +{ + Slot* slots; + GLFWmonitor* monitor = NULL; + int ch, i, width, height, count = 1; + + setlocale(LC_ALL, ""); + + glfwSetErrorCallback(error_callback); + + if (!glfwInit()) + exit(EXIT_FAILURE); + + printf("Library initialized\n"); + + while ((ch = getopt(argc, argv, "hfn:")) != -1) + { + switch (ch) + { + case 'h': + usage(); + exit(EXIT_SUCCESS); + + case 'f': + monitor = glfwGetPrimaryMonitor(); + break; + + case 'n': + count = (int) strtol(optarg, NULL, 10); + break; + + default: + usage(); + exit(EXIT_FAILURE); + } + } + + if (monitor) + { + const GLFWvidmode* mode = glfwGetVideoMode(monitor); + + glfwWindowHint(GLFW_REFRESH_RATE, mode->refreshRate); + glfwWindowHint(GLFW_RED_BITS, mode->redBits); + glfwWindowHint(GLFW_GREEN_BITS, mode->greenBits); + glfwWindowHint(GLFW_BLUE_BITS, mode->blueBits); + + width = mode->width; + height = mode->height; + } + else + { + width = 640; + height = 480; + } + + if (!count) + { + fprintf(stderr, "Invalid user\n"); + exit(EXIT_FAILURE); + } + + slots = calloc(count, sizeof(Slot)); + + for (i = 0; i < count; i++) + { + char title[128]; + + slots[i].closeable = GLFW_TRUE; + slots[i].number = i + 1; + + sprintf(title, "Event Linter (Window %i)", slots[i].number); + + if (monitor) + { + printf("Creating full screen window %i (%ix%i on %s)\n", + slots[i].number, + width, height, + glfwGetMonitorName(monitor)); + } + else + { + printf("Creating windowed mode window %i (%ix%i)\n", + slots[i].number, + width, height); + } + + slots[i].window = glfwCreateWindow(width, height, title, monitor, NULL); + if (!slots[i].window) + { + free(slots); + glfwTerminate(); + exit(EXIT_FAILURE); + } + + glfwSetWindowUserPointer(slots[i].window, slots + i); + + glfwSetMouseButtonCallback(slots[i].window, mouse_button_callback); + glfwSetCursorPosCallback(slots[i].window, cursor_position_callback); + + 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); + + glfwMakeContextCurrent(slots[i].window); + gladLoadGLLoader((GLADloadproc) glfwGetProcAddress); + glfwSwapInterval(1); + } + + printf("Main loop starting\n"); + printf("Left Mouse Button: toggle IME\n"); + printf("Right Mouse Button: set preedit cursor position\n\n"); + + for (;;) + { + for (i = 0; i < count; i++) + { + if (glfwWindowShouldClose(slots[i].window)) + break; + } + + if (i < count) + break; + + glfwWaitEvents(); + + // Workaround for an issue with msvcrt and mintty + fflush(stdout); + } + + free(slots); + glfwTerminate(); + exit(EXIT_SUCCESS); +} +