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.
This commit is contained in:
Stuart Carnie 2021-12-11 09:20:17 +11:00 committed by ocornut
parent f71ee5203e
commit 1b6b8602c1
5 changed files with 213 additions and 102 deletions

View File

@ -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);

View File

@ -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 <Cocoa/Cocoa.h>
#include <mach/mach_time.h>
#import <mach/mach_time.h>
#import <Carbon/Carbon.h>
#import <GameController/GameController.h>
// 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<NSTextInputClient>
@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<NSAttributedStringKey>*)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;
}

View File

@ -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

View File

@ -119,7 +119,7 @@
return event;
}];
ImGui_ImplOSX_Init();
ImGui_ImplOSX_Init(self.view);
#endif
}

View File

@ -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; }
//-----------------------------------------------------------------------------------