From 1b6b8602c1115276bc3cf1868e6a07571a566f97 Mon Sep 17 00:00:00 2001 From: Stuart Carnie Date: Sat, 11 Dec 2021 09:20:17 +1100 Subject: [PATCH] Backends: OSX: Fix keyboard support. Handle scroll cancel. Don't set mouse cursor shape unconditionally. (#4759, #4253, #1873) Note the original FIXME: refered to GLFWs Cocoa implementation, which is largely what this commit provides. --- backends/imgui_impl_osx.h | 7 +- backends/imgui_impl_osx.mm | 295 +++++++++++++++++-------- docs/CHANGELOG.txt | 6 + examples/example_apple_metal/main.mm | 2 +- examples/example_apple_opengl2/main.mm | 5 +- 5 files changed, 213 insertions(+), 102 deletions(-) diff --git a/backends/imgui_impl_osx.h b/backends/imgui_impl_osx.h index c223c2cc1..6cfe7eb2e 100644 --- a/backends/imgui_impl_osx.h +++ b/backends/imgui_impl_osx.h @@ -6,10 +6,9 @@ // [X] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. // [X] Platform: OSX clipboard is supported within core Dear ImGui (no specific code in this backend). // [X] Platform: Gamepad support. Enabled with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'. -// Issues: -// [ ] Platform: Keys are all generally very broken. Best using [event keycode] and not [event characters].. +// [X] Platform: Keyboard arrays indexed using kVK_* codes, e.g. ImGui::IsKeyPressed(kVK_Space). -// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. // If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. // Read online: https://github.com/ocornut/imgui/tree/master/docs @@ -19,7 +18,7 @@ @class NSEvent; @class NSView; -IMGUI_IMPL_API bool ImGui_ImplOSX_Init(); +IMGUI_IMPL_API bool ImGui_ImplOSX_Init(NSView* _Nonnull view); IMGUI_IMPL_API void ImGui_ImplOSX_Shutdown(); IMGUI_IMPL_API void ImGui_ImplOSX_NewFrame(NSView* _Nullable view); IMGUI_IMPL_API bool ImGui_ImplOSX_HandleEvent(NSEvent* _Nonnull event, NSView* _Nullable view); diff --git a/backends/imgui_impl_osx.mm b/backends/imgui_impl_osx.mm index 0c1b3dec2..e1222fa76 100644 --- a/backends/imgui_impl_osx.mm +++ b/backends/imgui_impl_osx.mm @@ -6,22 +6,23 @@ // [X] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. // [X] Platform: OSX clipboard is supported within core Dear ImGui (no specific code in this backend). // [X] Platform: Gamepad support. Enabled with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'. -// Issues: -// [ ] Platform: Keys are all generally very broken. Best using [event keycode] and not [event characters].. +// [X] Platform: Keyboard arrays indexed using kVK_* codes, e.g. ImGui::IsKeyPressed(kVK_Space). // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. // If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. // Read online: https://github.com/ocornut/imgui/tree/master/docs -#include "imgui.h" -#include "imgui_impl_osx.h" +#import "imgui.h" +#import "imgui_impl_osx.h" #import -#include +#import +#import #import // CHANGELOG // (minor and older changes stripped away, please see git history for details) +// 2021-12-13: *BREAKING CHANGE* Add NSView parameter to ImGui_ImplOSX_Init(). Generally fix keyboard support. Using kVK_* codes for keyboard keys. // 2021-12-13: Add game controller support. // 2021-09-21: Use mach_absolute_time as CFAbsoluteTimeGetCurrent can jump backwards. // 2021-08-17: Calling io.AddFocusEvent() on NSApplicationDidBecomeActiveNotification/NSApplicationDidResignActiveNotification events. @@ -40,15 +41,17 @@ // 2018-07-07: Initial version. @class ImFocusObserver; +@class KeyEventResponder; // Data -static double g_HostClockPeriod = 0.0; -static double g_Time = 0.0; -static NSCursor* g_MouseCursors[ImGuiMouseCursor_COUNT] = {}; -static bool g_MouseCursorHidden = false; -static bool g_MouseJustPressed[ImGuiMouseButton_COUNT] = {}; -static bool g_MouseDown[ImGuiMouseButton_COUNT] = {}; -static ImFocusObserver* g_FocusObserver = NULL; +static double g_HostClockPeriod = 0.0; +static double g_Time = 0.0; +static NSCursor* g_MouseCursors[ImGuiMouseCursor_COUNT] = {}; +static bool g_MouseCursorHidden = false; +static bool g_MouseJustPressed[ImGuiMouseButton_COUNT] = {}; +static bool g_MouseDown[ImGuiMouseButton_COUNT] = {}; +static ImFocusObserver* g_FocusObserver = nil; +static KeyEventResponder* g_KeyEventResponder = nil; // Undocumented methods for creating cursors. @interface NSCursor() @@ -77,6 +80,102 @@ static void resetKeys() io.KeyCtrl = io.KeyShift = io.KeyAlt = io.KeySuper = false; } +/** + KeyEventResponder implements the NSTextInputClient protocol as is required by the macOS text input manager. + + The macOS text input manager is invoked by calling the interpretKeyEvents method from the keyDown method. + Keyboard events are then evaluated by the macOS input manager and valid text input is passed back via the + insertText:replacementRange method. + + This is the same approach employed by other cross-platform libraries such as SDL2: + https://github.com/spurious/SDL-mirror/blob/e17aacbd09e65a4fd1e166621e011e581fb017a8/src/video/cocoa/SDL_cocoakeyboard.m#L53 + and GLFW: + https://github.com/glfw/glfw/blob/b55a517ae0c7b5127dffa79a64f5406021bf9076/src/cocoa_window.m#L722-L723 + */ +@interface KeyEventResponder: NSView +@end + +@implementation KeyEventResponder + +- (void)viewDidMoveToWindow +{ + // Ensure self is a first responder to receive the input events. + [self.window makeFirstResponder:self]; +} + +- (void)keyDown:(NSEvent*)event +{ + // Call to the macOS input manager system. + [self interpretKeyEvents:@[event]]; +} + +- (void)insertText:(id)aString replacementRange:(NSRange)replacementRange +{ + ImGuiIO& io = ImGui::GetIO(); + + NSString* characters; + if ([aString isKindOfClass:[NSAttributedString class]]) + characters = [aString string]; + else + characters = (NSString*)aString; + + io.AddInputCharactersUTF8(characters.UTF8String); +} + +- (BOOL)acceptsFirstResponder +{ + return YES; +} + +- (void)doCommandBySelector:(SEL)myselector +{ +} + +- (nullable NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range actualRange:(nullable NSRangePointer)actualRange +{ + return nil; +} + +- (NSUInteger)characterIndexForPoint:(NSPoint)point +{ + return 0; +} + +- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(nullable NSRangePointer)actualRange +{ + return NSZeroRect; +} + +- (BOOL)hasMarkedText +{ + return NO; +} + +- (NSRange)markedRange +{ + return NSMakeRange(NSNotFound, 0); +} + +- (NSRange)selectedRange +{ + return NSMakeRange(NSNotFound, 0); +} + +- (void)setMarkedText:(nonnull id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange +{ +} + +- (void)unmarkText +{ +} + +- (nonnull NSArray*)validAttributesForMarkedText +{ + return @[]; +} + +@end + @interface ImFocusObserver : NSObject - (void)onApplicationBecomeActive:(NSNotification*)aNotification; @@ -106,7 +205,7 @@ static void resetKeys() @end // Functions -bool ImGui_ImplOSX_Init() +bool ImGui_ImplOSX_Init(NSView* view) { ImGuiIO& io = ImGui::GetIO(); @@ -118,29 +217,28 @@ bool ImGui_ImplOSX_Init() io.BackendPlatformName = "imgui_impl_osx"; // Keyboard mapping. Dear ImGui will use those indices to peek into the io.KeyDown[] array. - const int offset_for_function_keys = 256 - 0xF700; - io.KeyMap[ImGuiKey_Tab] = '\t'; - io.KeyMap[ImGuiKey_LeftArrow] = NSLeftArrowFunctionKey + offset_for_function_keys; - io.KeyMap[ImGuiKey_RightArrow] = NSRightArrowFunctionKey + offset_for_function_keys; - io.KeyMap[ImGuiKey_UpArrow] = NSUpArrowFunctionKey + offset_for_function_keys; - io.KeyMap[ImGuiKey_DownArrow] = NSDownArrowFunctionKey + offset_for_function_keys; - io.KeyMap[ImGuiKey_PageUp] = NSPageUpFunctionKey + offset_for_function_keys; - io.KeyMap[ImGuiKey_PageDown] = NSPageDownFunctionKey + offset_for_function_keys; - io.KeyMap[ImGuiKey_Home] = NSHomeFunctionKey + offset_for_function_keys; - io.KeyMap[ImGuiKey_End] = NSEndFunctionKey + offset_for_function_keys; - io.KeyMap[ImGuiKey_Insert] = NSInsertFunctionKey + offset_for_function_keys; - io.KeyMap[ImGuiKey_Delete] = NSDeleteFunctionKey + offset_for_function_keys; - io.KeyMap[ImGuiKey_Backspace] = 127; - io.KeyMap[ImGuiKey_Space] = 32; - io.KeyMap[ImGuiKey_Enter] = 13; - io.KeyMap[ImGuiKey_Escape] = 27; - io.KeyMap[ImGuiKey_KeyPadEnter] = 3; - io.KeyMap[ImGuiKey_A] = 'A'; - io.KeyMap[ImGuiKey_C] = 'C'; - io.KeyMap[ImGuiKey_V] = 'V'; - io.KeyMap[ImGuiKey_X] = 'X'; - io.KeyMap[ImGuiKey_Y] = 'Y'; - io.KeyMap[ImGuiKey_Z] = 'Z'; + io.KeyMap[ImGuiKey_Tab] = kVK_Tab; + io.KeyMap[ImGuiKey_LeftArrow] = kVK_LeftArrow; + io.KeyMap[ImGuiKey_RightArrow] = kVK_RightArrow; + io.KeyMap[ImGuiKey_UpArrow] = kVK_UpArrow; + io.KeyMap[ImGuiKey_DownArrow] = kVK_DownArrow; + io.KeyMap[ImGuiKey_PageUp] = kVK_PageUp; + io.KeyMap[ImGuiKey_PageDown] = kVK_PageDown; + io.KeyMap[ImGuiKey_Home] = kVK_Home; + io.KeyMap[ImGuiKey_End] = kVK_End; + io.KeyMap[ImGuiKey_Insert] = kVK_F13; + io.KeyMap[ImGuiKey_Delete] = kVK_ForwardDelete; + io.KeyMap[ImGuiKey_Backspace] = kVK_Delete; + io.KeyMap[ImGuiKey_Space] = kVK_Space; + io.KeyMap[ImGuiKey_Enter] = kVK_Return; + io.KeyMap[ImGuiKey_Escape] = kVK_Escape; + io.KeyMap[ImGuiKey_KeyPadEnter] = kVK_ANSI_KeypadEnter; + io.KeyMap[ImGuiKey_A] = kVK_ANSI_A; + io.KeyMap[ImGuiKey_C] = kVK_ANSI_C; + io.KeyMap[ImGuiKey_V] = kVK_ANSI_V; + io.KeyMap[ImGuiKey_X] = kVK_ANSI_X; + io.KeyMap[ImGuiKey_Y] = kVK_ANSI_Y; + io.KeyMap[ImGuiKey_Z] = kVK_ANSI_Z; // Load cursors. Some of them are undocumented. g_MouseCursorHidden = false; @@ -193,6 +291,11 @@ bool ImGui_ImplOSX_Init() name:NSApplicationDidResignActiveNotification object:nil]; + // Add the NSTextInputClient to the view hierarchy, + // to receive keyboard events and translate them to input text. + g_KeyEventResponder = [[KeyEventResponder alloc] initWithFrame:NSZeroRect]; + [view addSubview:g_KeyEventResponder]; + return true; } @@ -227,8 +330,12 @@ static void ImGui_ImplOSX_UpdateMouseCursorAndButtons() } else { - // Show OS mouse cursor - [g_MouseCursors[g_MouseCursors[imgui_cursor] ? imgui_cursor : ImGuiMouseCursor_Arrow] set]; + NSCursor* desired = g_MouseCursors[imgui_cursor] ?: g_MouseCursors[ImGuiMouseCursor_Arrow]; + // -[NSCursor set] generates measureable overhead if called unconditionally. + if (desired != NSCursor.currentCursor) + { + [desired set]; + } if (g_MouseCursorHidden) { g_MouseCursorHidden = false; @@ -306,17 +413,22 @@ void ImGui_ImplOSX_NewFrame(NSView* view) ImGui_ImplOSX_UpdateGamepads(); } -static int mapCharacterToKey(int c) +NSString* NSStringFromPhase(NSEventPhase phase) { - if (c >= 'a' && c <= 'z') - return c - 'a' + 'A'; - if (c == 25) // SHIFT+TAB -> TAB - return 9; - if (c >= 0 && c < 256) - return c; - if (c >= 0xF700 && c < 0xF700 + 256) - return c - 0xF700 + 256; - return -1; + static NSString* strings[] = + { + @"none", + @"began", + @"stationary", + @"changed", + @"ended", + @"cancelled", + @"mayBegin", + }; + + int pos = phase == NSEventPhaseNone ? 0 : __builtin_ctzl((NSUInteger)phase) + 1; + + return strings[pos]; } bool ImGui_ImplOSX_HandleEvent(NSEvent* event, NSView* view) @@ -349,6 +461,21 @@ bool ImGui_ImplOSX_HandleEvent(NSEvent* event, NSView* view) if (event.type == NSEventTypeScrollWheel) { + // Ignore canceled events. + // + // From macOS 12.1, scrolling with two fingers and then decelerating + // by tapping two fingers results in two events appearing: + // + // 1. A scroll wheel NSEvent, with a phase == NSEventPhaseMayBegin, when the user taps + // two fingers to decelerate or stop the scroll events. + // + // 2. A scroll wheel NSEvent, with a phase == NSEventPhaseCancelled, when the user releases the + // two-finger tap. It is this event that sometimes contains large values for scrollingDeltaX and + // scrollingDeltaY. When these are added to the current x and y positions of the scrolling view, + // it appears to jump up or down. It can be observed in Preview, various JetBrains IDEs and here. + if (event.phase == NSEventPhaseCancelled) + return false; + double wheel_dx = 0.0; double wheel_dy = 0.0; @@ -370,6 +497,8 @@ bool ImGui_ImplOSX_HandleEvent(NSEvent* event, NSView* view) wheel_dy = [event deltaY]; } + //NSLog(@"dx=%0.3ff, dy=%0.3f, phase=%@", wheel_dx, wheel_dy, NSStringFromPhase(event.phase)); + if (fabs(wheel_dx) > 0.0) io.MouseWheelH += (float)wheel_dx * 0.1f; if (fabs(wheel_dy) > 0.0) @@ -377,57 +506,37 @@ bool ImGui_ImplOSX_HandleEvent(NSEvent* event, NSView* view) return io.WantCaptureMouse; } - // FIXME: All the key handling is wrong and broken. Refer to GLFW's cocoa_init.mm and cocoa_window.mm. - if (event.type == NSEventTypeKeyDown) + if (event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp) { - NSString* str = [event characters]; - NSUInteger len = [str length]; - for (NSUInteger i = 0; i < len; i++) - { - int c = [str characterAtIndex:i]; - if (!io.KeySuper && !(c >= 0xF700 && c <= 0xFFFF) && c != 127) - io.AddInputCharacter((unsigned int)c); - - // We must reset in case we're pressing a sequence of special keys while keeping the command pressed - int key = mapCharacterToKey(c); - if (key != -1 && key < 256 && !io.KeySuper) - resetKeys(); - if (key != -1) - io.KeysDown[key] = true; - } - return io.WantCaptureKeyboard; - } - - if (event.type == NSEventTypeKeyUp) - { - NSString* str = [event characters]; - NSUInteger len = [str length]; - for (NSUInteger i = 0; i < len; i++) - { - int c = [str characterAtIndex:i]; - int key = mapCharacterToKey(c); - if (key != -1) - io.KeysDown[key] = false; - } + unsigned short code = event.keyCode; + IM_ASSERT(code >= 0 && code < IM_ARRAYSIZE(io.KeysDown)); + io.KeysDown[code] = event.type == NSEventTypeKeyDown; + NSEventModifierFlags flags = event.modifierFlags; + io.KeyCtrl = (flags & NSEventModifierFlagControl) != 0; + io.KeyShift = (flags & NSEventModifierFlagShift) != 0; + io.KeyAlt = (flags & NSEventModifierFlagOption) != 0; + io.KeySuper = (flags & NSEventModifierFlagCommand) != 0; return io.WantCaptureKeyboard; } if (event.type == NSEventTypeFlagsChanged) { - unsigned int flags = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask; - - bool oldKeyCtrl = io.KeyCtrl; - bool oldKeyShift = io.KeyShift; - bool oldKeyAlt = io.KeyAlt; - bool oldKeySuper = io.KeySuper; - io.KeyCtrl = flags & NSEventModifierFlagControl; - io.KeyShift = flags & NSEventModifierFlagShift; - io.KeyAlt = flags & NSEventModifierFlagOption; - io.KeySuper = flags & NSEventModifierFlagCommand; - - // We must reset them as we will not receive any keyUp event if they where pressed with a modifier - if ((oldKeyShift && !io.KeyShift) || (oldKeyCtrl && !io.KeyCtrl) || (oldKeyAlt && !io.KeyAlt) || (oldKeySuper && !io.KeySuper)) - resetKeys(); + NSEventModifierFlags flags = event.modifierFlags; + switch (event.keyCode) + { + case kVK_Control: + io.KeyCtrl = (flags & NSEventModifierFlagControl) != 0; + break; + case kVK_Shift: + io.KeyShift = (flags & NSEventModifierFlagShift) != 0; + break; + case kVK_Option: + io.KeyAlt = (flags & NSEventModifierFlagOption) != 0; + break; + case kVK_Command: + io.KeySuper = (flags & NSEventModifierFlagCommand) != 0; + break; + } return io.WantCaptureKeyboard; } diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 638109dc3..639a49f01 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -38,6 +38,9 @@ Breaking Changes: - Removed CalcListClipping() function. Prefer using ImGuiListClipper which can return non-contiguous ranges. Please open an issue if you think you really need this function. (#3841) +- Backends: OSX: Added NSView* parameter to ImGui_ImplOSX_Init(). (#4759) [@stuartcarnie] + Updated Apple+Metal and Apple+GL example applications accordingly. + Other Changes: @@ -105,6 +108,9 @@ Other Changes: - Backends: DX12: Fixed DRAW_EMPTY_SCISSOR_RECTANGLE warnings. (#4775) - Backends: SDL_Renderer: Added support for large meshes (64k+ vertices) with 16-bit indices, enabling 'ImGuiBackendFlags_RendererHasVtxOffset' in the backend. (#3926) [@rokups] +- Backends: OSX: Generally fix keyboard support. Keyboard arrays indexed using kVK_* codes, e.g. + ImGui::IsKeyPressed(kVK_Space). Don't set mouse cursor shape unconditionally. Handle two fingers scroll + cancel event. (#4759, #4253, #1873) [@stuartcarnie] - Backends: OSX: Add Game Controller support (need linking GameController framework) (#4759) [@stuartcarnie] - Backends: WebGPU: Passing explicit buffer sizes to wgpuRenderPassEncoderSetVertexBuffer() and wgpuRenderPassEncoderSetIndexBuffer() functions as validation layers appears to not do what the diff --git a/examples/example_apple_metal/main.mm b/examples/example_apple_metal/main.mm index 5d4b7710c..bbe51d31f 100644 --- a/examples/example_apple_metal/main.mm +++ b/examples/example_apple_metal/main.mm @@ -119,7 +119,7 @@ return event; }]; - ImGui_ImplOSX_Init(); + ImGui_ImplOSX_Init(self.view); #endif } diff --git a/examples/example_apple_opengl2/main.mm b/examples/example_apple_opengl2/main.mm index 825c8a8f6..92774fbfc 100644 --- a/examples/example_apple_opengl2/main.mm +++ b/examples/example_apple_opengl2/main.mm @@ -58,7 +58,7 @@ //ImGui::StyleColorsClassic(); // Setup Platform/Renderer backends - ImGui_ImplOSX_Init(); + ImGui_ImplOSX_Init(self); ImGui_ImplOpenGL2_Init(); // Load Fonts @@ -149,9 +149,6 @@ -(void)reshape { [[self openGLContext] update]; [self updateAndDrawDemoView]; } -(void)drawRect:(NSRect)bounds { [self updateAndDrawDemoView]; } -(void)animationTimerFired:(NSTimer*)timer { [self setNeedsDisplay:YES]; } --(BOOL)acceptsFirstResponder { return (YES); } --(BOOL)becomeFirstResponder { return (YES); } --(BOOL)resignFirstResponder { return (YES); } -(void)dealloc { animationTimer = nil; } //-----------------------------------------------------------------------------------