@ -19,12 +19,14 @@
#import "imgui.h"
#import "imgui_impl_osx.h"
#import <Cocoa/Cocoa.h>
#import <mach/mach_time.h>
#import <Carbon/Carbon.h>
#import <GameController/GameController.h>
#import <time.h>
// CHANGELOG
// (minor and older changes stripped away, please see git history for details)
// 2022-05-03: Inputs: Removed ImGui_ImplOSX_HandleEvent() from backend API in favor of backend automatically handling event capture.
// 2022-04-27: Misc: Store backend data in a per-context struct, allowing to use this backend with multiple contexts.
// 2022-03-22: Inputs: Monitor NSKeyUp events to catch missing keyUp for key when user press Cmd + key
// 2022-02-07: Inputs: Forward keyDown/keyUp events to OS when unused by dear imgui.
// 2022-01-31: Fix building with old Xcode versions that are missing gamepad features.
@ -55,17 +57,32 @@
#define APPLE_HAS_CONTROLLER (__IPHONE_OS_VERSION_MIN_REQUIRED >= 140000 || __MAC_OS_X_VERSION_MIN_REQUIRED >= 110000 || __TV_OS_VERSION_MIN_REQUIRED >= 140000)
#define APPLE_HAS_THUMBSTICKS (__IPHONE_OS_VERSION_MIN_REQUIRED >= 120100 || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101401 || __TV_OS_VERSION_MIN_REQUIRED >= 120100)
@class ImFocus Observer;
@class ImGui Observer;
@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 ImFocusObserver* g_FocusObserver = nil;
static KeyEventResponder* g_KeyEventResponder = nil;
static NSTextInputContext* g_InputContext = nil;
struct ImGui_ImplOSX_Data
{
CFTimeInterval Time;
NSCursor* MouseCursors[ImGuiMouseCursor_COUNT];
bool MouseCursorHidden;
ImGuiObserver* Observer;
KeyEventResponder* KeyEventResponder;
NSTextInputContext* InputContext;
id Monitor;
ImGui_ImplOSX_Data() { memset(this, 0, sizeof(*this)); }
};
static ImGui_ImplOSX_Data* ImGui_ImplOSX_CreateBackendData() { return IM_NEW(ImGui_ImplOSX_Data)(); }
static ImGui_ImplOSX_Data* ImGui_ImplOSX_GetBackendData() { return (ImGui_ImplOSX_Data*)ImGui::GetIO().BackendPlatformUserData; }
static void ImGui_ImplOSX_DestroyBackendData() { IM_DELETE(ImGui_ImplOSX_GetBackendData()); }
static inline CFTimeInterval GetMachAbsoluteTimeInSeconds() { return static_cast<CFTimeInterval>(static_cast<double>(clock_gettime_nsec_np(CLOCK_UPTIME_RAW)) / 1e9); }
// Forward Declarations
static void ImGui_ImplOSX_AddTrackingArea(NSView* _Nonnull view);
static bool ImGui_ImplOSX_HandleEvent(NSEvent* event, NSView* view);
// Undocumented methods for creating cursors.
@interface NSCursor()
@ -75,18 +92,6 @@ static NSTextInputContext* g_InputContext = nil;
+ (id)_windowResizeEastWestCursor;
@end
static void InitHostClockPeriod()
{
struct mach_timebase_info info;
mach_timebase_info(&info);
g_HostClockPeriod = 1e-9 * ((double)info.denom / (double)info.numer); // Period is the reciprocal of frequency.
}
static double GetMachAbsoluteTimeInSeconds()
{
return (double)mach_absolute_time() * g_HostClockPeriod;
}
/**
KeyEventResponder implements the NSTextInputClient protocol as is required by the macOS text input manager.
@ -215,14 +220,14 @@ static double GetMachAbsoluteTimeInSeconds()
@end
@interface ImFocus Observer : NSObject
@interface ImGui Observer : NSObject
- (void)onApplicationBecomeActive:(NSNotification*)aNotification;
- (void)onApplicationBecomeInactive:(NSNotification*)aNotification;
@end
@implementation ImFocus Observer
@implementation ImGui Observer
- (void)onApplicationBecomeActive:(NSNotification*)aNotification
{
@ -365,6 +370,8 @@ static ImGuiKey ImGui_ImplOSX_KeyCodeToImGuiKey(int key_code)
bool ImGui_ImplOSX_Init(NSView* view)
{
ImGuiIO& io = ImGui::GetIO();
ImGui_ImplOSX_Data* bd = ImGui_ImplOSX_CreateBackendData();
io.BackendPlatformUserData = (void*)bd;
// Setup backend capabilities flags
io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values (optional)
@ -373,17 +380,19 @@ bool ImGui_ImplOSX_Init(NSView* view)
//io.BackendFlags |= ImGuiBackendFlags_HasMouseHoveredViewport; // We can call io.AddMouseViewportEvent() with correct data (optional)
io.BackendPlatformName = "imgui_impl_osx";
bd->Observer = [ImGuiObserver new];
// Load cursors. Some of them are undocumented.
g_MouseCursorHidden = false;
g_ MouseCursors[ImGuiMouseCursor_Arrow] = [NSCursor arrowCursor];
g_ MouseCursors[ImGuiMouseCursor_TextInput] = [NSCursor IBeamCursor];
g_ MouseCursors[ImGuiMouseCursor_ResizeAll] = [NSCursor closedHandCursor];
g_ MouseCursors[ImGuiMouseCursor_Hand] = [NSCursor pointingHandCursor];
g_ MouseCursors[ImGuiMouseCursor_NotAllowed] = [NSCursor operationNotAllowedCursor];
g_ MouseCursors[ImGuiMouseCursor_ResizeNS] = [NSCursor respondsToSelector:@selector(_windowResizeNorthSouthCursor)] ? [NSCursor _windowResizeNorthSouthCursor] : [NSCursor resizeUpDownCursor];
g_ MouseCursors[ImGuiMouseCursor_ResizeEW] = [NSCursor respondsToSelector:@selector(_windowResizeEastWestCursor)] ? [NSCursor _windowResizeEastWestCursor] : [NSCursor resizeLeftRightCursor];
g_ MouseCursors[ImGuiMouseCursor_ResizeNESW] = [NSCursor respondsToSelector:@selector(_windowResizeNorthEastSouthWestCursor)] ? [NSCursor _windowResizeNorthEastSouthWestCursor] : [NSCursor closedHandCursor];
g_ MouseCursors[ImGuiMouseCursor_ResizeNWSE] = [NSCursor respondsToSelector:@selector(_windowResizeNorthWestSouthEastCursor)] ? [NSCursor _windowResizeNorthWestSouthEastCursor] : [NSCursor closedHandCursor];
bd-> MouseCursorHidden = false;
bd-> MouseCursors[ImGuiMouseCursor_Arrow] = [NSCursor arrowCursor];
bd-> MouseCursors[ImGuiMouseCursor_TextInput] = [NSCursor IBeamCursor];
bd-> MouseCursors[ImGuiMouseCursor_ResizeAll] = [NSCursor closedHandCursor];
bd-> MouseCursors[ImGuiMouseCursor_Hand] = [NSCursor pointingHandCursor];
bd-> MouseCursors[ImGuiMouseCursor_NotAllowed] = [NSCursor operationNotAllowedCursor];
bd-> MouseCursors[ImGuiMouseCursor_ResizeNS] = [NSCursor respondsToSelector:@selector(_windowResizeNorthSouthCursor)] ? [NSCursor _windowResizeNorthSouthCursor] : [NSCursor resizeUpDownCursor];
bd-> MouseCursors[ImGuiMouseCursor_ResizeEW] = [NSCursor respondsToSelector:@selector(_windowResizeEastWestCursor)] ? [NSCursor _windowResizeEastWestCursor] : [NSCursor resizeLeftRightCursor];
bd-> MouseCursors[ImGuiMouseCursor_ResizeNESW] = [NSCursor respondsToSelector:@selector(_windowResizeNorthEastSouthWestCursor)] ? [NSCursor _windowResizeNorthEastSouthWestCursor] : [NSCursor closedHandCursor];
bd-> MouseCursors[ImGuiMouseCursor_ResizeNWSE] = [NSCursor respondsToSelector:@selector(_windowResizeNorthWestSouthEastCursor)] ? [NSCursor _windowResizeNorthWestSouthEastCursor] : [NSCursor closedHandCursor];
// Note that imgui.cpp also include default OSX clipboard handlers which can be enabled
// by adding '#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS' in imconfig.h and adding '-framework ApplicationServices' to your linker command-line.
@ -414,44 +423,36 @@ bool ImGui_ImplOSX_Init(NSView* view)
return s_clipboard.Data;
};
g_FocusObserver = [[ImFocusObserver alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:g_FocusObserver
[[NSNotificationCenter defaultCenter] addObserver:bd->Observer
selector:@selector(onApplicationBecomeActive:)
name:NSApplicationDidBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:g_Focus Observer
[[NSNotificationCenter defaultCenter] addObserver:bd-> Observer
selector:@selector(onApplicationBecomeInactive:)
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];
g_InputContext = [[NSTextInputContext alloc] initWithClient:g_KeyEventResponder];
[view addSubview:g_KeyEventResponder];
// Some events do not raise callbacks of AppView in some circumstances (for example when CMD key is held down).
// This monitor taps into global event stream and captures these events.
NSEventMask eventMask = NSEventMaskFromType(NSKeyUp) | NSEventMaskFlagsChanged;
[NSEvent addLocalMonitorForEventsMatchingMask:eventMask handler:^NSEvent * _Nullable(NSEvent *event)
{
ImGui_ImplOSX_HandleEvent(event, g_KeyEventResponder);
return event;
}];
bd->KeyEventResponder = [[KeyEventResponder alloc] initWithFrame:NSZeroRect];
bd->InputContext = [[NSTextInputContext alloc] initWithClient:bd->KeyEventResponder];
[view addSubview:bd->KeyEventResponder];
ImGui_ImplOSX_AddTrackingArea(view);
io.SetPlatformImeDataFn = [](ImGuiViewport* viewport, ImGuiPlatformImeData* data) -> void
{
ImGui_ImplOSX_Data* bd = ImGui_ImplOSX_GetBackendData();
if (data->WantVisible)
{
[g_ InputContext activate];
[bd->InputContext activate];
}
else
{
[g_ InputContext discardMarkedText];
[g_ InputContext invalidateCharacterCoordinates];
[g_ InputContext deactivate];
[bd-> InputContext discardMarkedText];
[bd-> InputContext invalidateCharacterCoordinates];
[bd-> InputContext deactivate];
}
[g_ KeyEventResponder setImePosX:data->InputPos.x imePosY:data->InputPos.y + data->InputLineHeight];
[bd-> KeyEventResponder setImePosX:data->InputPos.x imePosY:data->InputPos.y + data->InputLineHeight];
};
return true;
@ -459,11 +460,19 @@ bool ImGui_ImplOSX_Init(NSView* view)
void ImGui_ImplOSX_Shutdown()
{
g_FocusObserver = NULL;
ImGui_ImplOSX_Data* bd = ImGui_ImplOSX_GetBackendData();
bd->Observer = NULL;
if (bd->Monitor != NULL)
{
[NSEvent removeMonitor:bd->Monitor];
bd->Monitor = NULL;
}
ImGui_ImplOSX_DestroyBackendData();
}
static void ImGui_ImplOSX_UpdateMouseCursor()
{
ImGui_ImplOSX_Data* bd = ImGui_ImplOSX_GetBackendData();
ImGuiIO& io = ImGui::GetIO();
if (io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange)
return;
@ -472,23 +481,23 @@ static void ImGui_ImplOSX_UpdateMouseCursor()
if (io.MouseDrawCursor || imgui_cursor == ImGuiMouseCursor_None)
{
// Hide OS mouse cursor if imgui is drawing it or if it wants no cursor
if (!g_ MouseCursorHidden)
if (!bd-> MouseCursorHidden)
{
g_ MouseCursorHidden = true;
bd-> MouseCursorHidden = true;
[NSCursor hide];
}
}
else
{
NSCursor* desired = g_MouseCursors[imgui_cursor] ?: g_ MouseCursors[ImGuiMouseCursor_Arrow];
NSCursor* desired = bd->MouseCursors[imgui_cursor] ?: bd-> MouseCursors[ImGuiMouseCursor_Arrow];
// -[NSCursor set] generates measureable overhead if called unconditionally.
if (desired != NSCursor.currentCursor)
{
[desired set];
}
if (g_ MouseCursorHidden)
if (bd-> MouseCursorHidden)
{
g_ MouseCursorHidden = false;
bd-> MouseCursorHidden = false;
[NSCursor unhide];
}
}
@ -555,15 +564,18 @@ static void ImGui_ImplOSX_UpdateGamepads()
static void ImGui_ImplOSX_UpdateImePosWithView(NSView* view)
{
ImGui_ImplOSX_Data* bd = ImGui_ImplOSX_GetBackendData();
ImGuiIO& io = ImGui::GetIO();
if (io.WantTextInput)
[g_ KeyEventResponder updateImePosWithView:view];
[bd-> KeyEventResponder updateImePosWithView:view];
}
void ImGui_ImplOSX_NewFrame(NSView* view)
{
// Setup display size
ImGui_ImplOSX_Data* bd = ImGui_ImplOSX_GetBackendData();
ImGuiIO& io = ImGui::GetIO();
// Setup display size
if (view)
{
const float dpi = (float)[view.window backingScaleFactor];
@ -572,21 +584,19 @@ void ImGui_ImplOSX_NewFrame(NSView* view)
}
// Setup time step
if (g_Time == 0.0)
{
InitHostClockPeriod();
g_Time = GetMachAbsoluteTimeInSeconds();
}
if (bd->Time == 0.0)
bd->Time = GetMachAbsoluteTimeInSeconds();
double current_time = GetMachAbsoluteTimeInSeconds();
io.DeltaTime = (float)(current_time - g_ Time);
g_ Time = current_time;
io.DeltaTime = (float)(current_time - bd->Time);
bd-> Time = current_time;
ImGui_ImplOSX_UpdateMouseCursor();
ImGui_ImplOSX_UpdateGamepads();
ImGui_ImplOSX_UpdateImePosWithView(view);
}
bool ImGui_ImplOSX_HandleEvent(NSEvent* event, NSView* view)
static bool ImGui_ImplOSX_HandleEvent(NSEvent* event, NSView* view)
{
ImGuiIO& io = ImGui::GetIO();
@ -711,3 +721,27 @@ bool ImGui_ImplOSX_HandleEvent(NSEvent* event, NSView* view)
return false;
}
static void ImGui_ImplOSX_AddTrackingArea(NSView* _Nonnull view)
{
// If we want to receive key events, we either need to be in the responder chain of the key view,
// or else we can install a local monitor. The consequence of this heavy-handed approach is that
// we receive events for all controls, not just Dear ImGui widgets. If we had native controls in our
// window, we'd want to be much more careful than just ingesting the complete event stream.
// To match the behavior of other backends, we pass every event down to the OS.
ImGui_ImplOSX_Data* bd = ImGui_ImplOSX_GetBackendData();
if (bd->Monitor)
return;
NSEventMask eventMask = 0;
eventMask |= NSEventMaskMouseMoved | NSEventMaskScrollWheel;
eventMask |= NSEventMaskLeftMouseDown | NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged;
eventMask |= NSEventMaskRightMouseDown | NSEventMaskRightMouseUp | NSEventMaskRightMouseDragged;
eventMask |= NSEventMaskOtherMouseDown | NSEventMaskOtherMouseUp | NSEventMaskOtherMouseDragged;
eventMask |= NSEventMaskKeyDown | NSEventMaskKeyUp | NSEventMaskFlagsChanged;
bd->Monitor = [NSEvent addLocalMonitorForEventsMatchingMask:eventMask
handler:^NSEvent* _Nullable(NSEvent* event)
{
ImGui_ImplOSX_HandleEvent(event, view);
return event;
}];
}