kitty/glfw/cocoa_window.m
pagedown 9a598237c6
macOS: Allow IME to actively get the cursor position in real time
IME will automatically get the display position when needed, which keeps
it consistent with the overlay as much as possible.

Fix the issue that when IME is activated after mouse click, it is
displayed at the wrong position.
2023-02-22 22:36:20 +08:00

2989 lines
105 KiB
Objective-C

//========================================================================
// GLFW 3.4 macOS - www.glfw.org
//------------------------------------------------------------------------
// Copyright (c) 2009-2019 Camilla Löwy <elmindreda@glfw.org>
//
// 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.
//
//========================================================================
// It is fine to use C99 in this file because it will not be built with VS
//========================================================================
#include "internal.h"
#include "../kitty/monotonic.h"
#include <Availability.h>
#import <CoreServices/CoreServices.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#include <float.h>
#include <string.h>
static const char*
polymorphic_string_as_utf8(id string) {
if (string == nil) return "(nil)";
NSString* characters;
if ([string isKindOfClass:[NSAttributedString class]])
characters = [string string];
else
characters = (NSString*) string;
return [characters UTF8String];
}
static uint32_t
vk_code_to_functional_key_code(uint8_t key_code) { // {{{
switch(key_code) {
/* start vk to functional (auto generated by gen-key-constants.py do not edit) */
case 0x35: return GLFW_FKEY_ESCAPE;
case 0x24: return GLFW_FKEY_ENTER;
case 0x30: return GLFW_FKEY_TAB;
case 0x33: return GLFW_FKEY_BACKSPACE;
case 0x72: return GLFW_FKEY_INSERT;
case 0x75: return GLFW_FKEY_DELETE;
case 0x7b: return GLFW_FKEY_LEFT;
case 0x7c: return GLFW_FKEY_RIGHT;
case 0x7e: return GLFW_FKEY_UP;
case 0x7d: return GLFW_FKEY_DOWN;
case 0x74: return GLFW_FKEY_PAGE_UP;
case 0x79: return GLFW_FKEY_PAGE_DOWN;
case 0x73: return GLFW_FKEY_HOME;
case 0x77: return GLFW_FKEY_END;
case 0x39: return GLFW_FKEY_CAPS_LOCK;
case 0x47: return GLFW_FKEY_NUM_LOCK;
case 0x6e: return GLFW_FKEY_MENU;
case 0x7a: return GLFW_FKEY_F1;
case 0x78: return GLFW_FKEY_F2;
case 0x63: return GLFW_FKEY_F3;
case 0x76: return GLFW_FKEY_F4;
case 0x60: return GLFW_FKEY_F5;
case 0x61: return GLFW_FKEY_F6;
case 0x62: return GLFW_FKEY_F7;
case 0x64: return GLFW_FKEY_F8;
case 0x65: return GLFW_FKEY_F9;
case 0x6d: return GLFW_FKEY_F10;
case 0x67: return GLFW_FKEY_F11;
case 0x6f: return GLFW_FKEY_F12;
case 0x69: return GLFW_FKEY_F13;
case 0x6b: return GLFW_FKEY_F14;
case 0x71: return GLFW_FKEY_F15;
case 0x6a: return GLFW_FKEY_F16;
case 0x40: return GLFW_FKEY_F17;
case 0x4f: return GLFW_FKEY_F18;
case 0x50: return GLFW_FKEY_F19;
case 0x5a: return GLFW_FKEY_F20;
case 0x52: return GLFW_FKEY_KP_0;
case 0x53: return GLFW_FKEY_KP_1;
case 0x54: return GLFW_FKEY_KP_2;
case 0x55: return GLFW_FKEY_KP_3;
case 0x56: return GLFW_FKEY_KP_4;
case 0x57: return GLFW_FKEY_KP_5;
case 0x58: return GLFW_FKEY_KP_6;
case 0x59: return GLFW_FKEY_KP_7;
case 0x5b: return GLFW_FKEY_KP_8;
case 0x5c: return GLFW_FKEY_KP_9;
case 0x41: return GLFW_FKEY_KP_DECIMAL;
case 0x4b: return GLFW_FKEY_KP_DIVIDE;
case 0x43: return GLFW_FKEY_KP_MULTIPLY;
case 0x4e: return GLFW_FKEY_KP_SUBTRACT;
case 0x45: return GLFW_FKEY_KP_ADD;
case 0x4c: return GLFW_FKEY_KP_ENTER;
case 0x51: return GLFW_FKEY_KP_EQUAL;
case 0x38: return GLFW_FKEY_LEFT_SHIFT;
case 0x3b: return GLFW_FKEY_LEFT_CONTROL;
case 0x3a: return GLFW_FKEY_LEFT_ALT;
case 0x37: return GLFW_FKEY_LEFT_SUPER;
case 0x3c: return GLFW_FKEY_RIGHT_SHIFT;
case 0x3e: return GLFW_FKEY_RIGHT_CONTROL;
case 0x3d: return GLFW_FKEY_RIGHT_ALT;
case 0x36: return GLFW_FKEY_RIGHT_SUPER;
/* end vk to functional */
default:
return 0;
}
} // }}}
static uint32_t
vk_code_to_unicode(uint8_t key_code) { // {{{
switch(key_code) {
/* start vk to unicode (auto generated by gen-key-constants.py do not edit) */
case 0x0: return 0x61;
case 0x1: return 0x73;
case 0x2: return 0x64;
case 0x3: return 0x66;
case 0x4: return 0x68;
case 0x5: return 0x67;
case 0x6: return 0x7a;
case 0x7: return 0x78;
case 0x8: return 0x63;
case 0x9: return 0x76;
case 0xb: return 0x62;
case 0xc: return 0x71;
case 0xd: return 0x77;
case 0xe: return 0x65;
case 0xf: return 0x72;
case 0x10: return 0x79;
case 0x11: return 0x74;
case 0x12: return 0x31;
case 0x13: return 0x32;
case 0x14: return 0x33;
case 0x15: return 0x34;
case 0x16: return 0x36;
case 0x17: return 0x35;
case 0x18: return 0x3d;
case 0x19: return 0x39;
case 0x1a: return 0x37;
case 0x1b: return 0x2d;
case 0x1c: return 0x38;
case 0x1d: return 0x30;
case 0x1e: return 0x5d;
case 0x1f: return 0x6f;
case 0x20: return 0x75;
case 0x21: return 0x5b;
case 0x22: return 0x69;
case 0x23: return 0x70;
case 0x25: return 0x6c;
case 0x26: return 0x6a;
case 0x27: return 0x27;
case 0x28: return 0x6b;
case 0x29: return 0x3b;
case 0x2a: return 0x5c;
case 0x2b: return 0x2c;
case 0x2c: return 0x2f;
case 0x2d: return 0x6e;
case 0x2e: return 0x6d;
case 0x2f: return 0x2e;
case 0x31: return 0x20;
case 0x32: return 0x60;
/* end vk to unicode */
default:
return 0;
}
} // }}}
static uint32_t
mac_ucode_to_functional(uint32_t key_code) { // {{{
switch(key_code) {
/* start macu to functional (auto generated by gen-key-constants.py do not edit) */
case NSCarriageReturnCharacter: return GLFW_FKEY_ENTER;
case NSTabCharacter: return GLFW_FKEY_TAB;
case NSBackspaceCharacter: return GLFW_FKEY_BACKSPACE;
case NSInsertFunctionKey: return GLFW_FKEY_INSERT;
case NSDeleteFunctionKey: return GLFW_FKEY_DELETE;
case NSLeftArrowFunctionKey: return GLFW_FKEY_LEFT;
case NSRightArrowFunctionKey: return GLFW_FKEY_RIGHT;
case NSUpArrowFunctionKey: return GLFW_FKEY_UP;
case NSDownArrowFunctionKey: return GLFW_FKEY_DOWN;
case NSPageUpFunctionKey: return GLFW_FKEY_PAGE_UP;
case NSPageDownFunctionKey: return GLFW_FKEY_PAGE_DOWN;
case NSHomeFunctionKey: return GLFW_FKEY_HOME;
case NSEndFunctionKey: return GLFW_FKEY_END;
case NSScrollLockFunctionKey: return GLFW_FKEY_SCROLL_LOCK;
case NSClearLineFunctionKey: return GLFW_FKEY_NUM_LOCK;
case NSPrintScreenFunctionKey: return GLFW_FKEY_PRINT_SCREEN;
case NSPauseFunctionKey: return GLFW_FKEY_PAUSE;
case NSMenuFunctionKey: return GLFW_FKEY_MENU;
case NSF1FunctionKey: return GLFW_FKEY_F1;
case NSF2FunctionKey: return GLFW_FKEY_F2;
case NSF3FunctionKey: return GLFW_FKEY_F3;
case NSF4FunctionKey: return GLFW_FKEY_F4;
case NSF5FunctionKey: return GLFW_FKEY_F5;
case NSF6FunctionKey: return GLFW_FKEY_F6;
case NSF7FunctionKey: return GLFW_FKEY_F7;
case NSF8FunctionKey: return GLFW_FKEY_F8;
case NSF9FunctionKey: return GLFW_FKEY_F9;
case NSF10FunctionKey: return GLFW_FKEY_F10;
case NSF11FunctionKey: return GLFW_FKEY_F11;
case NSF12FunctionKey: return GLFW_FKEY_F12;
case NSF13FunctionKey: return GLFW_FKEY_F13;
case NSF14FunctionKey: return GLFW_FKEY_F14;
case NSF15FunctionKey: return GLFW_FKEY_F15;
case NSF16FunctionKey: return GLFW_FKEY_F16;
case NSF17FunctionKey: return GLFW_FKEY_F17;
case NSF18FunctionKey: return GLFW_FKEY_F18;
case NSF19FunctionKey: return GLFW_FKEY_F19;
case NSF20FunctionKey: return GLFW_FKEY_F20;
case NSF21FunctionKey: return GLFW_FKEY_F21;
case NSF22FunctionKey: return GLFW_FKEY_F22;
case NSF23FunctionKey: return GLFW_FKEY_F23;
case NSF24FunctionKey: return GLFW_FKEY_F24;
case NSF25FunctionKey: return GLFW_FKEY_F25;
case NSF26FunctionKey: return GLFW_FKEY_F26;
case NSF27FunctionKey: return GLFW_FKEY_F27;
case NSF28FunctionKey: return GLFW_FKEY_F28;
case NSF29FunctionKey: return GLFW_FKEY_F29;
case NSF30FunctionKey: return GLFW_FKEY_F30;
case NSF31FunctionKey: return GLFW_FKEY_F31;
case NSF32FunctionKey: return GLFW_FKEY_F32;
case NSF33FunctionKey: return GLFW_FKEY_F33;
case NSF34FunctionKey: return GLFW_FKEY_F34;
case NSF35FunctionKey: return GLFW_FKEY_F35;
case NSEnterCharacter: return GLFW_FKEY_KP_ENTER;
/* end macu to functional */
default:
return 0;
}
} // }}}
static bool
is_surrogate(UniChar uc) { return (uc - 0xd800u) < 2048u; }
static uint32_t
get_first_codepoint(UniChar *utf16, UniCharCount num) {
if (!num) return 0;
if (!is_surrogate(*utf16)) return *utf16;
if (CFStringIsSurrogateHighCharacter(*utf16) && num > 1 && CFStringIsSurrogateLowCharacter(utf16[1])) return CFStringGetLongCharacterForSurrogatePair(utf16[0], utf16[1]);
return 0;
}
static bool
is_pua_char(uint32_t ch) {
return (0xE000 <= ch && ch <= 0xF8FF) || (0xF0000 <= ch && ch <= 0xFFFFF) || (0x100000 <= ch && ch <= 0x10FFFF);
}
uint32_t
vk_to_unicode_key_with_current_layout(uint16_t keycode)
{
UInt32 dead_key_state = 0;
UniChar characters[256];
UniCharCount character_count = 0;
uint32_t ans = vk_code_to_functional_key_code(keycode);
if (ans) return ans;
if (UCKeyTranslate([(NSData*) _glfw.ns.unicodeData bytes],
keycode,
kUCKeyActionDisplay,
0,
LMGetKbdType(),
kUCKeyTranslateNoDeadKeysBit,
&dead_key_state,
arraysz(characters),
&character_count,
characters) == noErr) {
uint32_t cp = get_first_codepoint(characters, character_count);
if (cp) {
if (cp < 32 || (0xF700 <= cp && cp <= 0xF8FF)) return mac_ucode_to_functional(cp);
if (cp >= 32 && !is_pua_char(cp)) return cp;
}
}
return vk_code_to_unicode(keycode);
}
// Returns the style mask corresponding to the window settings
//
static NSUInteger getStyleMask(_GLFWwindow* window)
{
NSUInteger styleMask = NSWindowStyleMaskMiniaturizable;
if (window->monitor || !window->decorated)
styleMask |= NSWindowStyleMaskBorderless;
else
{
styleMask |= NSWindowStyleMaskTitled |
NSWindowStyleMaskClosable;
if (window->resizable)
styleMask |= NSWindowStyleMaskResizable;
}
return styleMask;
}
CGDirectDisplayID displayIDForWindow(_GLFWwindow *w) {
NSWindow *nw = w->ns.object;
NSDictionary *dict = [nw.screen deviceDescription];
NSNumber *displayIDns = dict[@"NSScreenNumber"];
if (displayIDns) return [displayIDns unsignedIntValue];
return (CGDirectDisplayID)-1;
}
static unsigned long long display_link_shutdown_timer = 0;
#define DISPLAY_LINK_SHUTDOWN_CHECK_INTERVAL s_to_monotonic_t(30ll)
void
_glfwShutdownCVDisplayLink(unsigned long long timer_id UNUSED, void *user_data UNUSED) {
display_link_shutdown_timer = 0;
for (size_t i = 0; i < _glfw.ns.displayLinks.count; i++) {
_GLFWDisplayLinkNS *dl = &_glfw.ns.displayLinks.entries[i];
if (dl->displayLink) CVDisplayLinkStop(dl->displayLink);
dl->lastRenderFrameRequestedAt = 0;
dl->first_unserviced_render_frame_request_at = 0;
}
}
static void
requestRenderFrame(_GLFWwindow *w, GLFWcocoarenderframefun callback) {
if (!callback) {
w->ns.renderFrameRequested = false;
w->ns.renderFrameCallback = NULL;
return;
}
w->ns.renderFrameCallback = callback;
w->ns.renderFrameRequested = true;
CGDirectDisplayID displayID = displayIDForWindow(w);
if (display_link_shutdown_timer) {
_glfwPlatformUpdateTimer(display_link_shutdown_timer, DISPLAY_LINK_SHUTDOWN_CHECK_INTERVAL, true);
} else {
display_link_shutdown_timer = _glfwPlatformAddTimer(DISPLAY_LINK_SHUTDOWN_CHECK_INTERVAL, false, _glfwShutdownCVDisplayLink, NULL, NULL);
}
monotonic_t now = glfwGetTime();
bool found_display_link = false;
_GLFWDisplayLinkNS *dl = NULL;
for (size_t i = 0; i < _glfw.ns.displayLinks.count; i++) {
dl = &_glfw.ns.displayLinks.entries[i];
if (dl->displayID == displayID) {
found_display_link = true;
dl->lastRenderFrameRequestedAt = now;
if (!dl->first_unserviced_render_frame_request_at) dl->first_unserviced_render_frame_request_at = now;
if (!CVDisplayLinkIsRunning(dl->displayLink)) CVDisplayLinkStart(dl->displayLink);
else if (now - dl->first_unserviced_render_frame_request_at > s_to_monotonic_t(1ll)) {
// display link is stuck need to recreate it because Apple cant even
// get a simple timer right
CVDisplayLinkRelease(dl->displayLink); dl->displayLink = nil;
dl->first_unserviced_render_frame_request_at = now;
_glfw_create_cv_display_link(dl);
_glfwInputError(GLFW_PLATFORM_ERROR,
"CVDisplayLink stuck possibly because of sleep/screensaver + Apple's incompetence, recreating.");
if (!CVDisplayLinkIsRunning(dl->displayLink)) CVDisplayLinkStart(dl->displayLink);
}
} else if (dl->displayLink && dl->lastRenderFrameRequestedAt && now - dl->lastRenderFrameRequestedAt >= DISPLAY_LINK_SHUTDOWN_CHECK_INTERVAL) {
CVDisplayLinkStop(dl->displayLink);
dl->lastRenderFrameRequestedAt = 0;
dl->first_unserviced_render_frame_request_at = 0;
}
}
if (!found_display_link) {
dl = _glfw_create_display_link(displayID);
if (dl) {
dl->lastRenderFrameRequestedAt = now;
dl->first_unserviced_render_frame_request_at = now;
if (!CVDisplayLinkIsRunning(dl->displayLink)) CVDisplayLinkStart(dl->displayLink);
}
}
}
void
_glfwRestartDisplayLinks(void) {
_GLFWwindow* window;
for (window = _glfw.windowListHead; window; window = window->next) {
if (window->ns.renderFrameRequested && window->ns.renderFrameCallback) {
requestRenderFrame(window, window->ns.renderFrameCallback);
}
}
}
// Returns whether the cursor is in the content area of the specified window
//
static bool cursorInContentArea(_GLFWwindow* window)
{
const NSPoint pos = [window->ns.object mouseLocationOutsideOfEventStream];
return [window->ns.view mouse:pos inRect:[window->ns.view frame]];
}
// Hides the cursor if not already hidden
//
static void hideCursor(_GLFWwindow* window UNUSED)
{
if (!_glfw.ns.cursorHidden)
{
[NSCursor hide];
_glfw.ns.cursorHidden = true;
}
}
// Shows the cursor if not already shown
//
static void showCursor(_GLFWwindow* window UNUSED)
{
if (_glfw.ns.cursorHidden)
{
[NSCursor unhide];
_glfw.ns.cursorHidden = false;
}
}
// Updates the cursor image according to its cursor mode
//
static void updateCursorImage(_GLFWwindow* window)
{
if (window->cursorMode == GLFW_CURSOR_NORMAL)
{
showCursor(window);
if (window->cursor)
[(NSCursor*) window->cursor->ns.object set];
else
[[NSCursor arrowCursor] set];
}
else
hideCursor(window);
}
// Apply chosen cursor mode to a focused window
//
static void updateCursorMode(_GLFWwindow* window)
{
if (window->cursorMode == GLFW_CURSOR_DISABLED)
{
_glfw.ns.disabledCursorWindow = window;
_glfwPlatformGetCursorPos(window,
&_glfw.ns.restoreCursorPosX,
&_glfw.ns.restoreCursorPosY);
_glfwCenterCursorInContentArea(window);
CGAssociateMouseAndMouseCursorPosition(false);
}
else if (_glfw.ns.disabledCursorWindow == window)
{
_glfw.ns.disabledCursorWindow = NULL;
CGAssociateMouseAndMouseCursorPosition(true);
_glfwPlatformSetCursorPos(window,
_glfw.ns.restoreCursorPosX,
_glfw.ns.restoreCursorPosY);
}
if (cursorInContentArea(window))
updateCursorImage(window);
}
// Make the specified window and its video mode active on its monitor
//
static void acquireMonitor(_GLFWwindow* window)
{
_glfwSetVideoModeNS(window->monitor, &window->videoMode);
const CGRect bounds = CGDisplayBounds(window->monitor->ns.displayID);
const NSRect frame = NSMakeRect(bounds.origin.x,
_glfwTransformYNS(bounds.origin.y + bounds.size.height - 1),
bounds.size.width,
bounds.size.height);
[window->ns.object setFrame:frame display:YES];
_glfwInputMonitorWindow(window->monitor, window);
}
// Remove the window and restore the original video mode
//
static void releaseMonitor(_GLFWwindow* window)
{
if (window->monitor->window != window)
return;
_glfwInputMonitorWindow(window->monitor, NULL);
_glfwRestoreVideoModeNS(window->monitor);
}
// Translates macOS key modifiers into GLFW ones
//
static int
translateFlags(NSUInteger flags)
{
int mods = 0;
if (flags & NSEventModifierFlagShift)
mods |= GLFW_MOD_SHIFT;
if (flags & NSEventModifierFlagControl)
mods |= GLFW_MOD_CONTROL;
if (flags & NSEventModifierFlagOption)
mods |= GLFW_MOD_ALT;
if (flags & NSEventModifierFlagCommand)
mods |= GLFW_MOD_SUPER;
if (flags & NSEventModifierFlagCapsLock)
mods |= GLFW_MOD_CAPS_LOCK;
return mods;
}
static const char*
format_mods(int mods) {
static char buf[128];
char *p = buf, *s;
#define pr(x) p += snprintf(p, sizeof(buf) - (p - buf) - 1, x)
pr("mods: ");
s = p;
if (mods & GLFW_MOD_CONTROL) pr("ctrl+");
if (mods & GLFW_MOD_ALT) pr("alt+");
if (mods & GLFW_MOD_SHIFT) pr("shift+");
if (mods & GLFW_MOD_SUPER) pr("super+");
if (mods & GLFW_MOD_CAPS_LOCK) pr("capslock+");
if (mods & GLFW_MOD_NUM_LOCK) pr("numlock+");
if (p == s) pr("none");
else p--;
pr(" ");
#undef pr
return buf;
}
static const char*
format_text(const char *src) {
static char buf[256];
char *p = buf;
const char *last_char = buf + sizeof(buf) - 1;
if (!src[0]) return "<none>";
while (*src) {
int num = snprintf(p, sizeof(buf) - (p - buf), "0x%x ", (unsigned char)*(src++));
if (num < 0) return "<error>";
if (p + num >= last_char) break;
p += num;
}
if (p != buf) *(--p) = 0;
return buf;
}
static const char*
safe_name_for_keycode(unsigned int keycode) {
const char *ans = _glfwPlatformGetNativeKeyName(keycode);
if (!ans) return "<noname>";
if ((1 <= ans[0] && ans[0] <= 31) || ans[0] == 127) ans = "<cc>";
return ans;
}
// Translates a macOS keycode to a GLFW keycode
//
static uint32_t
translateKey(uint16_t vk_key, bool apply_keymap)
{
if (apply_keymap) return vk_to_unicode_key_with_current_layout(vk_key);
uint32_t ans = vk_code_to_functional_key_code(vk_key);
if (!ans) ans = vk_code_to_unicode(vk_key);
return ans;
}
static NSRect
get_window_size_without_border_in_logical_pixels(_GLFWwindow *window) {
return [window->ns.object contentRectForFrameRect:[window->ns.object frame]];
}
// Defines a constant for empty ranges in NSTextInputClient
//
static const NSRange kEmptyRange = { NSNotFound, 0 };
// Delegate for window related notifications {{{
@interface GLFWWindowDelegate : NSObject
{
_GLFWwindow* window;
}
- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow;
- (void)request_delayed_cursor_update:(id)sender;
@end
@implementation GLFWWindowDelegate
- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow
{
self = [super init];
if (self != nil)
window = initWindow;
return self;
}
- (BOOL)windowShouldClose:(id)sender
{
(void)sender;
_glfwInputWindowCloseRequest(window);
return NO;
}
- (void)windowDidResize:(NSNotification *)notification
{
(void)notification;
if (window->context.client != GLFW_NO_API)
[window->context.nsgl.object update];
if (_glfw.ns.disabledCursorWindow == window)
_glfwCenterCursorInContentArea(window);
const int maximized = [window->ns.object isZoomed];
if (window->ns.maximized != maximized)
{
window->ns.maximized = maximized;
_glfwInputWindowMaximize(window, maximized);
}
const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
const NSRect fbRect = [window->ns.view convertRectToBacking:contentRect];
if (fbRect.size.width != window->ns.fbWidth ||
fbRect.size.height != window->ns.fbHeight)
{
window->ns.fbWidth = (int)fbRect.size.width;
window->ns.fbHeight = (int)fbRect.size.height;
_glfwInputFramebufferSize(window, (int)fbRect.size.width, (int)fbRect.size.height);
}
if (contentRect.size.width != window->ns.width ||
contentRect.size.height != window->ns.height)
{
window->ns.width = (int)contentRect.size.width;
window->ns.height = (int)contentRect.size.height;
_glfwInputWindowSize(window, (int)contentRect.size.width, (int)contentRect.size.height);
}
}
- (void)windowDidMove:(NSNotification *)notification
{
(void)notification;
if (window->context.client != GLFW_NO_API)
[window->context.nsgl.object update];
if (_glfw.ns.disabledCursorWindow == window)
_glfwCenterCursorInContentArea(window);
int x, y;
_glfwPlatformGetWindowPos(window, &x, &y);
_glfwInputWindowPos(window, x, y);
}
- (void)windowDidChangeOcclusionState:(NSNotification *)notification
{
(void)notification;
_glfwInputWindowOcclusion(window, !([window->ns.object occlusionState] & NSWindowOcclusionStateVisible));
}
- (void)windowDidMiniaturize:(NSNotification *)notification
{
(void)notification;
if (window->monitor)
releaseMonitor(window);
_glfwInputWindowIconify(window, true);
}
- (void)windowDidDeminiaturize:(NSNotification *)notification
{
(void)notification;
if (window->monitor)
acquireMonitor(window);
_glfwInputWindowIconify(window, false);
}
- (void)windowDidBecomeKey:(NSNotification *)notification
{
(void)notification;
if (_glfw.ns.disabledCursorWindow == window)
_glfwCenterCursorInContentArea(window);
_glfwInputWindowFocus(window, true);
updateCursorMode(window);
if (window->cursorMode == GLFW_CURSOR_HIDDEN) hideCursor(window);
if (_glfw.ns.disabledCursorWindow != window && cursorInContentArea(window))
{
double x = 0, y = 0;
_glfwPlatformGetCursorPos(window, &x, &y);
_glfwInputCursorPos(window, x, y);
}
// macOS will send a delayed event to update the cursor to arrow after switching desktops.
// So we need to delay and update the cursor once after that.
[self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3];
}
- (void)windowDidResignKey:(NSNotification *)notification
{
(void)notification;
if (window->monitor && window->autoIconify)
_glfwPlatformIconifyWindow(window);
showCursor(window);
_glfwInputWindowFocus(window, false);
// IME is cancelled when losing the focus
if ([window->ns.view hasMarkedText]) {
[[window->ns.view inputContext] discardMarkedText];
[window->ns.view unmarkText];
GLFWkeyevent dummy = {.action = GLFW_RELEASE, .ime_state = GLFW_IME_PREEDIT_CHANGED};
_glfwInputKeyboard(window, &dummy);
_glfw.ns.text[0] = 0;
}
}
- (void)windowDidChangeScreen:(NSNotification *)notification
{
(void)notification;
if (window->ns.renderFrameRequested && window->ns.renderFrameCallback) {
// Ensure that if the window changed its monitor, CVDisplayLink
// is running for the new monitor
requestRenderFrame(window, window->ns.renderFrameCallback);
}
}
- (void)request_delayed_cursor_update:(id)sender
{
(void)sender;
if (window) window->ns.delayed_cursor_update_requested = true;
}
- (void)windowWillEnterFullScreen:(NSNotification *)notification
{
(void)notification;
if (window) window->ns.in_fullscreen_transition = true;
}
- (void)windowDidEnterFullScreen:(NSNotification *)notification
{
(void)notification;
if (window) window->ns.in_fullscreen_transition = false;
[self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3];
}
- (void)windowWillExitFullScreen:(NSNotification *)notification
{
(void)notification;
if (window) window->ns.in_fullscreen_transition = true;
}
- (void)windowDidExitFullScreen:(NSNotification *)notification
{
(void)notification;
if (window) window->ns.in_fullscreen_transition = false;
[self performSelector:@selector(request_delayed_cursor_update:) withObject:nil afterDelay:0.3];
}
@end // }}}
// Text input context class for the GLFW content view {{{
@interface GLFWTextInputContext : NSTextInputContext
@end
@implementation GLFWTextInputContext
- (void)doCommandBySelector:(SEL)selector
{
// interpretKeyEvents: May call insertText: or doCommandBySelector:.
// With the default macOS keybindings, pressing certain key combinations
// (e.g. Ctrl+/, Ctrl+Cmd+Down/Left/Right) will produce a beep sound.
debug_key("\n\tTextInputCtx: doCommandBySelector: (%s)\n", [NSStringFromSelector(selector) UTF8String]);
}
@end // }}}
// Content view class for the GLFW window {{{
@interface GLFWContentView : NSView <NSTextInputClient>
{
_GLFWwindow* window;
NSTrackingArea* trackingArea;
GLFWTextInputContext* input_context;
NSMutableAttributedString* markedText;
NSRect markedRect;
bool marked_text_cleared_by_insert;
int in_key_handler;
NSString *input_source_at_last_key_event;
}
- (void) removeGLFWWindow;
- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow;
@end
@implementation GLFWContentView
- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow
{
self = [super init];
if (self != nil)
{
window = initWindow;
trackingArea = nil;
input_context = [[GLFWTextInputContext alloc] initWithClient:self];
markedText = [[NSMutableAttributedString alloc] init];
markedRect = NSMakeRect(0.0, 0.0, 0.0, 0.0);
input_source_at_last_key_event = nil;
in_key_handler = 0;
[self updateTrackingAreas];
[self registerForDraggedTypes:@[NSPasteboardTypeFileURL, NSPasteboardTypeString]];
}
return self;
}
- (void)dealloc
{
[trackingArea release];
[markedText release];
if (input_source_at_last_key_event) [input_source_at_last_key_event release];
[input_context release];
[super dealloc];
}
- (void) removeGLFWWindow
{
window = NULL;
}
- (_GLFWwindow*)glfwWindow {
return window;
}
- (BOOL)isOpaque
{
return window && [window->ns.object isOpaque];
}
- (BOOL)canBecomeKeyView
{
return YES;
}
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (void) viewWillStartLiveResize
{
if (!window) return;
_glfwInputLiveResize(window, true);
}
- (void)viewDidEndLiveResize
{
if (!window) return;
_glfwInputLiveResize(window, false);
}
- (BOOL)wantsUpdateLayer
{
return YES;
}
- (void)updateLayer
{
if (!window) return;
if (window->context.client != GLFW_NO_API) {
@try {
[window->context.nsgl.object update];
} @catch (NSException *e) {
_glfwInputError(GLFW_PLATFORM_ERROR,
"Failed to update NSGL Context object with error: %s (%s)",
[[e name] UTF8String], [[e reason] UTF8String]);
}
}
_glfwInputWindowDamage(window);
}
- (void)cursorUpdate:(NSEvent *)event
{
(void)event;
if (window) updateCursorImage(window);
}
- (BOOL)acceptsFirstMouse:(NSEvent *)event
{
(void)event;
return NO; // changed by Kovid, to follow cocoa platform conventions
}
- (void)mouseDown:(NSEvent *)event
{
if (!window) return;
_glfwInputMouseClick(window,
GLFW_MOUSE_BUTTON_LEFT,
GLFW_PRESS,
translateFlags([event modifierFlags]));
}
- (void)mouseDragged:(NSEvent *)event
{
[self mouseMoved:event];
}
- (void)mouseUp:(NSEvent *)event
{
if (!window) return;
_glfwInputMouseClick(window,
GLFW_MOUSE_BUTTON_LEFT,
GLFW_RELEASE,
translateFlags([event modifierFlags]));
}
- (void)mouseMoved:(NSEvent *)event
{
if (!window) return;
if (window->cursorMode == GLFW_CURSOR_DISABLED)
{
const double dx = [event deltaX] - window->ns.cursorWarpDeltaX;
const double dy = [event deltaY] - window->ns.cursorWarpDeltaY;
_glfwInputCursorPos(window,
window->virtualCursorPosX + dx,
window->virtualCursorPosY + dy);
}
else
{
const NSRect contentRect = [window->ns.view frame];
// NOTE: The returned location uses base 0,1 not 0,0
const NSPoint pos = [event locationInWindow];
_glfwInputCursorPos(window, pos.x, contentRect.size.height - pos.y);
}
window->ns.cursorWarpDeltaX = 0;
window->ns.cursorWarpDeltaY = 0;
if (window->ns.delayed_cursor_update_requested) {
window->ns.delayed_cursor_update_requested = false;
if (cursorInContentArea(window)) updateCursorImage(window);
}
}
- (void)rightMouseDown:(NSEvent *)event
{
if (!window) return;
_glfwInputMouseClick(window,
GLFW_MOUSE_BUTTON_RIGHT,
GLFW_PRESS,
translateFlags([event modifierFlags]));
}
- (void)rightMouseDragged:(NSEvent *)event
{
[self mouseMoved:event];
}
- (void)rightMouseUp:(NSEvent *)event
{
if (!window) return;
_glfwInputMouseClick(window,
GLFW_MOUSE_BUTTON_RIGHT,
GLFW_RELEASE,
translateFlags([event modifierFlags]));
}
- (void)otherMouseDown:(NSEvent *)event
{
if (!window) return;
_glfwInputMouseClick(window,
(int) [event buttonNumber],
GLFW_PRESS,
translateFlags([event modifierFlags]));
}
- (void)otherMouseDragged:(NSEvent *)event
{
[self mouseMoved:event];
}
- (void)otherMouseUp:(NSEvent *)event
{
if (!window) return;
_glfwInputMouseClick(window,
(int) [event buttonNumber],
GLFW_RELEASE,
translateFlags([event modifierFlags]));
}
- (void)mouseExited:(NSEvent *)event
{
(void)event;
if (!window) return;
_glfwInputCursorEnter(window, false);
}
- (void)mouseEntered:(NSEvent *)event
{
(void)event;
if (!window) return;
_glfwInputCursorEnter(window, true);
}
- (void)viewDidChangeBackingProperties
{
if (!window) return;
const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
const NSRect fbRect = [window->ns.view convertRectToBacking:contentRect];
if (fbRect.size.width != window->ns.fbWidth ||
fbRect.size.height != window->ns.fbHeight)
{
window->ns.fbWidth = (int)fbRect.size.width;
window->ns.fbHeight = (int)fbRect.size.height;
_glfwInputFramebufferSize(window, (int)fbRect.size.width, (int)fbRect.size.height);
}
const float xscale = fbRect.size.width / contentRect.size.width;
const float yscale = fbRect.size.height / contentRect.size.height;
if (xscale != window->ns.xscale || yscale != window->ns.yscale)
{
window->ns.xscale = xscale;
window->ns.yscale = yscale;
_glfwInputWindowContentScale(window, xscale, yscale);
if (window->ns.retina && window->ns.layer)
[window->ns.layer setContentsScale:[window->ns.object backingScaleFactor]];
}
}
- (void)drawRect:(NSRect)rect
{
(void)rect;
if (!window) return;
_glfwInputWindowDamage(window);
}
- (void)updateTrackingAreas
{
if (window && [window->ns.object areCursorRectsEnabled])
[window->ns.object disableCursorRects];
if (trackingArea != nil)
{
[self removeTrackingArea:trackingArea];
[trackingArea release];
}
const NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited |
NSTrackingActiveAlways |
NSTrackingEnabledDuringMouseDrag |
NSTrackingCursorUpdate |
NSTrackingInVisibleRect |
NSTrackingAssumeInside;
trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds]
options:options
owner:self
userInfo:nil];
[self addTrackingArea:trackingArea];
[super updateTrackingAreas];
}
- (NSTextInputContext *)inputContext
{
return input_context;
}
static UInt32
convert_cocoa_to_carbon_modifiers(NSUInteger flags) {
UInt32 mods = 0;
if (flags & NSEventModifierFlagShift)
mods |= shiftKey;
if (flags & NSEventModifierFlagControl)
mods |= controlKey;
if (flags & NSEventModifierFlagOption)
mods |= optionKey;
if (flags & NSEventModifierFlagCommand)
mods |= cmdKey;
if (flags & NSEventModifierFlagCapsLock)
mods |= alphaLock;
return (mods >> 8) & 0xFF;
}
static void
convert_utf16_to_utf8(UniChar *src, UniCharCount src_length, char *dest, size_t dest_sz) {
CFStringRef string = CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
src,
src_length,
kCFAllocatorNull);
CFStringGetCString(string,
dest,
dest_sz,
kCFStringEncodingUTF8);
CFRelease(string);
}
static bool
alternate_key_is_ok(uint32_t key, uint32_t akey) {
return akey > 31 && akey != key && !is_pua_char(akey);
}
static void
add_alternate_keys(GLFWkeyevent *ev, NSEvent *event) {
ev->alternate_key = translateKey(ev->native_key, false);
if (!alternate_key_is_ok(ev->key, ev->alternate_key)) ev->alternate_key = 0;
if (ev->mods & GLFW_MOD_SHIFT) {
NSString *ci = [event charactersIgnoringModifiers];
if (ci) {
unsigned sz = [ci length];
if (sz > 0) {
UniChar buf[2] = {0};
buf[0] = [ci characterAtIndex:0];
if (sz > 1) buf[1] = [ci characterAtIndex:1];
ev->shifted_key = get_first_codepoint(buf, sz);
}
}
if (!alternate_key_is_ok(ev->key, ev->shifted_key)) ev->shifted_key = 0;
}
}
static bool
is_ascii_control_char(char x) {
return x == 0 || (1 <= x && x <= 31) || x == 127;
}
- (void)keyDown:(NSEvent *)event
{
#define CLEAR_PRE_EDIT_TEXT glfw_keyevent.text = NULL; glfw_keyevent.ime_state = GLFW_IME_PREEDIT_CHANGED; _glfwInputKeyboard(window, &glfw_keyevent);
#define UPDATE_PRE_EDIT_TEXT glfw_keyevent.text = [[markedText string] UTF8String]; glfw_keyevent.ime_state = GLFW_IME_PREEDIT_CHANGED; _glfwInputKeyboard(window, &glfw_keyevent);
const bool previous_has_marked_text = [self hasMarkedText];
if (input_context && (!input_source_at_last_key_event || ![input_source_at_last_key_event isEqualToString:input_context.selectedKeyboardInputSource])) {
if (input_source_at_last_key_event) {
debug_key("Input source changed, clearing pre-edit text and resetting deadkey state\n");
GLFWkeyevent dummy = {.action = GLFW_RELEASE, .ime_state = GLFW_IME_PREEDIT_CHANGED};
window->ns.deadKeyState = 0;
_glfwInputKeyboard(window, &dummy); // clear pre-edit text
[input_source_at_last_key_event release];
input_source_at_last_key_event = nil;
}
input_source_at_last_key_event = [input_context.selectedKeyboardInputSource retain];
[self unmarkText];
}
const unsigned int keycode = [event keyCode];
const NSUInteger flags = [event modifierFlags];
const int mods = translateFlags(flags);
const uint32_t key = translateKey(keycode, true);
const bool process_text = !_glfw.ignoreOSKeyboardProcessing && (!window->ns.textInputFilterCallback || window->ns.textInputFilterCallback(key, mods, keycode, flags) != 1);
_glfw.ns.text[0] = 0;
if (keycode == 0x33 /* backspace */ || keycode == 0x35 /* escape */) [self unmarkText];
GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = GLFW_PRESS, .mods = mods};
if (!_glfw.ns.unicodeData) {
// Using the cocoa API for key handling is disabled, as there is no
// reliable way to handle dead keys using it. Only use it if the
// keyboard unicode data is not available.
if (process_text) {
// this will call insertText with the text for this event, if any
[self interpretKeyEvents:@[event]];
}
} else {
static UniChar text[256];
UniCharCount char_count = 0;
const bool in_compose_sequence = window->ns.deadKeyState != 0;
if (UCKeyTranslate(
[(NSData*) _glfw.ns.unicodeData bytes],
keycode,
kUCKeyActionDown,
convert_cocoa_to_carbon_modifiers(flags),
LMGetKbdType(),
(process_text ? 0 : kUCKeyTranslateNoDeadKeysMask),
&(window->ns.deadKeyState),
sizeof(text)/sizeof(text[0]),
&char_count,
text
) != noErr) {
debug_key("UCKeyTranslate failed for keycode: 0x%x (%s) %s\n",
keycode, safe_name_for_keycode(keycode), format_mods(mods));
window->ns.deadKeyState = 0;
return;
}
debug_key("\x1b[31mPress:\x1b[m native_key: 0x%x (%s) glfw_key: 0x%x %schar_count: %lu deadKeyState: %u repeat: %d ",
keycode, safe_name_for_keycode(keycode), key, format_mods(mods), char_count, window->ns.deadKeyState, event.ARepeat);
marked_text_cleared_by_insert = false;
if (process_text) {
in_key_handler = 1;
// this will call insertText which will fill up _glfw.ns.text
[self interpretKeyEvents:@[event]];
in_key_handler = 0;
} else {
window->ns.deadKeyState = 0;
}
if (window->ns.deadKeyState && (char_count == 0 || keycode == 0x75)) {
// 0x75 is the delete key which needs to be ignored during a compose sequence
debug_key("Sending pre-edit text for dead key (text: %s markedText: %s).\n", format_text(_glfw.ns.text), glfw_keyevent.text);
UPDATE_PRE_EDIT_TEXT;
return;
}
if (in_compose_sequence) {
debug_key("Clearing pre-edit text at end of compose sequence\n");
CLEAR_PRE_EDIT_TEXT;
}
}
if (is_ascii_control_char(_glfw.ns.text[0])) _glfw.ns.text[0] = 0; // don't send text for ascii control codes
debug_key("text: %s glfw_key: %s marked_text: (%s)\n",
format_text(_glfw.ns.text), _glfwGetKeyName(key), [[markedText string] UTF8String]);
bool bracketed_ime = false;
if (!window->ns.deadKeyState) {
if ([self hasMarkedText]) {
if (!marked_text_cleared_by_insert) {
UPDATE_PRE_EDIT_TEXT;
} else bracketed_ime = true;
} else if (previous_has_marked_text) {
CLEAR_PRE_EDIT_TEXT;
}
if (([self hasMarkedText] || previous_has_marked_text) && !_glfw.ns.text[0]) {
// do not pass keys like BACKSPACE while there's pre-edit text, let IME handle it
debug_key("Ignoring key press as IME is active and it generated no text\n");
return;
}
}
if (bracketed_ime) {
// insertText followed by setMarkedText
CLEAR_PRE_EDIT_TEXT;
}
glfw_keyevent.text = _glfw.ns.text;
glfw_keyevent.ime_state = GLFW_IME_NONE;
add_alternate_keys(&glfw_keyevent, event);
_glfwInputKeyboard(window, &glfw_keyevent);
if (bracketed_ime) {
// insertText followed by setMarkedText
UPDATE_PRE_EDIT_TEXT;
}
}
- (void)flagsChanged:(NSEvent *)event
{
int action = GLFW_RELEASE;
const char old_first_char = _glfw.ns.text[0];
_glfw.ns.text[0] = 0;
const NSUInteger modifierFlags = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask;
const uint32_t key = vk_code_to_functional_key_code([event keyCode]);
const unsigned int keycode = [event keyCode];
const int mods = translateFlags(modifierFlags);
const bool process_text = !_glfw.ignoreOSKeyboardProcessing && (!window->ns.textInputFilterCallback || window->ns.textInputFilterCallback(key, mods, keycode, modifierFlags) != 1);
const char *mod_name = "unknown";
switch(key) {
case GLFW_FKEY_CAPS_LOCK:
mod_name = "capslock";
action = modifierFlags & NSEventModifierFlagCapsLock ? GLFW_PRESS : GLFW_RELEASE; break;
case GLFW_FKEY_LEFT_SUPER:
case GLFW_FKEY_RIGHT_SUPER:
mod_name = "super";
action = modifierFlags & NSEventModifierFlagCommand ? GLFW_PRESS : GLFW_RELEASE; break;
case GLFW_FKEY_LEFT_CONTROL:
case GLFW_FKEY_RIGHT_CONTROL:
mod_name = "ctrl";
action = modifierFlags & NSEventModifierFlagControl ? GLFW_PRESS : GLFW_RELEASE; break;
case GLFW_FKEY_LEFT_ALT:
case GLFW_FKEY_RIGHT_ALT:
mod_name = "alt";
action = modifierFlags & NSEventModifierFlagOption ? GLFW_PRESS : GLFW_RELEASE; break;
case GLFW_FKEY_LEFT_SHIFT:
case GLFW_FKEY_RIGHT_SHIFT:
mod_name = "shift";
action = modifierFlags & NSEventModifierFlagShift ? GLFW_PRESS : GLFW_RELEASE; break;
default:
return;
}
GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = action, .mods = mods};
debug_key("\x1b[33mflagsChanged:\x1b[m modifier: %s native_key: 0x%x (%s) glfw_key: 0x%x %s\n",
mod_name, keycode, safe_name_for_keycode(keycode), key, format_mods(mods));
marked_text_cleared_by_insert = false;
if (process_text && input_context) {
// this will call insertText which will fill up _glfw.ns.text
in_key_handler = 2;
[input_context handleEvent:event];
in_key_handler = 0;
if (marked_text_cleared_by_insert) {
debug_key("Clearing pre-edit text because insertText called from flagsChanged\n");
CLEAR_PRE_EDIT_TEXT;
if (_glfw.ns.text[0]) glfw_keyevent.text = _glfw.ns.text;
else _glfw.ns.text[0] = old_first_char;
}
}
glfw_keyevent.ime_state = GLFW_IME_NONE;
_glfwInputKeyboard(window, &glfw_keyevent);
}
- (void)keyUp:(NSEvent *)event
{
const uint32_t keycode = [event keyCode];
const uint32_t key = translateKey(keycode, true);
const int mods = translateFlags([event modifierFlags]);
GLFWkeyevent glfw_keyevent = {.key = key, .native_key = keycode, .native_key_id = keycode, .action = GLFW_RELEASE, .mods = mods};
add_alternate_keys(&glfw_keyevent, event);
debug_key("\x1b[32mRelease:\x1b[m native_key: 0x%x (%s) glfw_key: 0x%x %s\n",
keycode, safe_name_for_keycode(keycode), key, format_mods(mods));
_glfwInputKeyboard(window, &glfw_keyevent);
}
#undef CLEAR_PRE_EDIT_TEXT
#undef UPDATE_PRE_EDIT_TEXT
- (void)scrollWheel:(NSEvent *)event
{
double deltaX = [event scrollingDeltaX];
double deltaY = [event scrollingDeltaY];
int flags = [event hasPreciseScrollingDeltas] ? 1 : 0;
if (flags) {
float xscale = 1, yscale = 1;
_glfwPlatformGetWindowContentScale(window, &xscale, &yscale);
if (xscale > 0) deltaX *= xscale;
if (yscale > 0) deltaY *= yscale;
}
switch([event momentumPhase]) {
case NSEventPhaseBegan:
flags |= (1 << 1); break;
case NSEventPhaseStationary:
flags |= (2 << 1); break;
case NSEventPhaseChanged:
flags |= (3 << 1); break;
case NSEventPhaseEnded:
flags |= (4 << 1); break;
case NSEventPhaseCancelled:
flags |= (5 << 1); break;
case NSEventPhaseMayBegin:
flags |= (6 << 1); break;
case NSEventPhaseNone:
default:
break;
}
_glfwInputScroll(window, deltaX, deltaY, flags, translateFlags([event modifierFlags]));
}
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
{
(void)sender;
// HACK: We don't know what to say here because we don't know what the
// application wants to do with the paths
return NSDragOperationGeneric;
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
const NSRect contentRect = [window->ns.view frame];
// NOTE: The returned location uses base 0,1 not 0,0
const NSPoint pos = [sender draggingLocation];
_glfwInputCursorPos(window, pos.x, contentRect.size.height - pos.y);
NSPasteboard* pasteboard = [sender draggingPasteboard];
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
NSArray* objs = [pasteboard readObjectsForClasses:@[[NSURL class], [NSString class]]
options:options];
if (!objs) return NO;
const NSUInteger count = [objs count];
NSMutableString *uri_list = [NSMutableString stringWithCapacity:4096]; // auto-released
if (count)
{
for (NSUInteger i = 0; i < count; i++)
{
id obj = objs[i];
if ([obj isKindOfClass:[NSURL class]]) {
NSURL *url = (NSURL*)obj;
if ([uri_list length] > 0) [uri_list appendString:@("\n")];
if (url.fileURL) [uri_list appendString:url.filePathURL.absoluteString];
else [uri_list appendString:url.absoluteString];
} else if ([obj isKindOfClass:[NSString class]]) {
const char *text = [obj UTF8String];
_glfwInputDrop(window, "text/plain;charset=utf-8", text, strlen(text));
} else {
_glfwInputError(GLFW_PLATFORM_ERROR,
"Cocoa: Object is neither a URL nor a string");
}
}
}
if ([uri_list length] > 0) _glfwInputDrop(window, "text/uri-list", uri_list.UTF8String, strlen(uri_list.UTF8String));
return YES;
}
- (BOOL)hasMarkedText
{
return [markedText length] > 0;
}
- (NSRange)markedRange
{
if ([markedText length] > 0)
return NSMakeRange(0, [markedText length] - 1);
else
return kEmptyRange;
}
- (NSRange)selectedRange
{
return kEmptyRange;
}
- (void)setMarkedText:(id)string
selectedRange:(NSRange)selectedRange
replacementRange:(NSRange)replacementRange
{
const char *s = polymorphic_string_as_utf8(string);
debug_key("\n\tsetMarkedText: %s selectedRange: (%lu, %lu) replacementRange: (%lu, %lu)\n", s, selectedRange.location, selectedRange.length, replacementRange.location, replacementRange.length);
if (string == nil || !s[0]) {
bool had_marked_text = [self hasMarkedText];
[self unmarkText];
if (had_marked_text && (!in_key_handler || in_key_handler == 2)) {
debug_key("Clearing pre-edit because setMarkedText called from %s\n", in_key_handler ? "flagsChanged" : "event loop");
GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED};
_glfwInputKeyboard(window, &glfw_keyevent);
_glfw.ns.text[0] = 0;
}
return;
}
if ([string isKindOfClass:[NSAttributedString class]]) {
if (((NSMutableAttributedString*)string).length == 0) { [self unmarkText]; return; }
[markedText release];
markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string];
} else {
if (((NSString*)string).length == 0) { [self unmarkText]; return; }
[markedText release];
markedText = [[NSMutableAttributedString alloc] initWithString:string];
}
if (!in_key_handler || in_key_handler == 2) {
debug_key("Updating IME text in kitty from setMarkedText called from %s: %s\n", in_key_handler ? "flagsChanged" : "event loop", _glfw.ns.text);
GLFWkeyevent glfw_keyevent = {.text=[[markedText string] UTF8String], .ime_state = GLFW_IME_PREEDIT_CHANGED};
_glfwInputKeyboard(window, &glfw_keyevent);
_glfw.ns.text[0] = 0;
}
}
- (void)unmarkText
{
[[markedText mutableString] setString:@""];
}
void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
[w->ns.view updateIMEStateFor: ev->type focused:(bool)ev->focused];
}
- (void)updateIMEStateFor:(GLFWIMEUpdateType)which
focused:(bool)focused
{
if (which == GLFW_IME_UPDATE_FOCUS && !focused && [self hasMarkedText] && window) {
[input_context discardMarkedText];
[self unmarkText];
GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED};
_glfwInputKeyboard(window, &glfw_keyevent);
_glfw.ns.text[0] = 0;
}
if (which != GLFW_IME_UPDATE_CURSOR_POSITION) return;
if (_glfwPlatformWindowFocused(window)) [[window->ns.view inputContext] invalidateCharacterCoordinates];
}
- (NSArray*)validAttributesForMarkedText
{
return [NSArray array];
}
- (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
actualRange:(NSRangePointer)actualRange
{
(void)range; (void)actualRange;
return nil;
}
- (NSUInteger)characterIndexForPoint:(NSPoint)point
{
(void)point;
return 0;
}
- (NSRect)firstRectForCharacterRange:(NSRange)range
actualRange:(NSRangePointer)actualRange
{
(void)range; (void)actualRange;
if (_glfw.callbacks.get_ime_cursor_position) {
GLFWIMEUpdateEvent ev = { .type = GLFW_IME_UPDATE_CURSOR_POSITION };
if (_glfw.callbacks.get_ime_cursor_position((GLFWwindow*)window, &ev)) {
const CGFloat left = (CGFloat)ev.cursor.left / window->ns.xscale;
const CGFloat top = (CGFloat)ev.cursor.top / window->ns.yscale;
const CGFloat cellWidth = (CGFloat)ev.cursor.width / window->ns.xscale;
const CGFloat cellHeight = (CGFloat)ev.cursor.height / window->ns.yscale;
debug_key("updateIMEPosition: left=%f, top=%f, width=%f, height=%f\n", left, top, cellWidth, cellHeight);
const NSRect frame = [window->ns.view frame];
const NSRect rectInView = NSMakeRect(left,
frame.size.height - top - cellHeight,
cellWidth, cellHeight);
markedRect = [window->ns.object convertRectToScreen: rectInView];
}
}
return markedRect;
}
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange
{
const char *utf8 = polymorphic_string_as_utf8(string);
debug_key("\n\tinsertText: %s replacementRange: (%lu, %lu)\n", utf8, replacementRange.location, replacementRange.length);
if ([self hasMarkedText] && !is_ascii_control_char(utf8[0])) {
[self unmarkText];
marked_text_cleared_by_insert = true;
if (!in_key_handler) {
debug_key("Clearing pre-edit because insertText called from event loop\n");
GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED};
_glfwInputKeyboard(window, &glfw_keyevent);
_glfw.ns.text[0] = 0;
}
}
// insertText can be called multiple times for a single key event
char *s = _glfw.ns.text + strnlen(_glfw.ns.text, sizeof(_glfw.ns.text));
snprintf(s, sizeof(_glfw.ns.text) - (s - _glfw.ns.text), "%s", utf8);
_glfw.ns.text[sizeof(_glfw.ns.text) - 1] = 0;
if ((!in_key_handler || in_key_handler == 2) && _glfw.ns.text[0]) {
if (!is_ascii_control_char(_glfw.ns.text[0])) {
debug_key("Sending text to kitty from insertText called from %s: %s\n", in_key_handler ? "flagsChanged" : "event loop", _glfw.ns.text);
GLFWkeyevent glfw_keyevent = {.text=_glfw.ns.text, .ime_state=GLFW_IME_COMMIT_TEXT};
_glfwInputKeyboard(window, &glfw_keyevent);
}
_glfw.ns.text[0] = 0;
}
}
- (void)doCommandBySelector:(SEL)selector
{
debug_key("\n\tdoCommandBySelector: (%s)\n", [NSStringFromSelector(selector) UTF8String]);
}
- (BOOL)isAccessibilityElement
{
return YES;
}
- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector
{
if (selector == @selector(accessibilityRole) || selector == @selector(accessibilitySelectedText)) return YES;
return NO;
}
#if (TARGET_OS_OSX && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101400)
- (NSAccessibilityRole)accessibilityRole
{
return NSAccessibilityTextAreaRole;
}
#endif
- (NSString *)accessibilitySelectedText
{
NSString *text = nil;
if (_glfw.callbacks.get_current_selection) {
char *s = _glfw.callbacks.get_current_selection();
if (s) {
text = [NSString stringWithUTF8String:s];
free(s);
}
}
return text;
}
// <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/SysServices/Articles/using.html>
// Support services receiving "public.utf8-plain-text" and "NSStringPboardType"
- (id)validRequestorForSendType:(NSString *)sendType returnType:(NSString *)returnType
{
if (
(!sendType || [sendType isEqual:NSPasteboardTypeString] || [sendType isEqual:@"NSStringPboardType"]) &&
(!returnType || [returnType isEqual:NSPasteboardTypeString] || [returnType isEqual:@"NSStringPboardType"])
) {
if (_glfw.callbacks.has_current_selection && _glfw.callbacks.has_current_selection()) return self;
}
return [super validRequestorForSendType:sendType returnType:returnType];
}
// Selected text as input to be sent to Services
- (BOOL)writeSelectionToPasteboard:(NSPasteboard *)pboard types:(NSArray *)types
{
if (!_glfw.callbacks.get_current_selection) return NO;
char *text = _glfw.callbacks.get_current_selection();
if (!text) return NO;
BOOL ans = NO;
if (text[0]) {
if ([types containsObject:NSPasteboardTypeString] == YES) {
[pboard declareTypes:@[NSPasteboardTypeString] owner:self];
ans = [pboard setString:@(text) forType:NSPasteboardTypeString];
} else if ([types containsObject:@"NSStringPboardType"] == YES) {
[pboard declareTypes:@[@"NSStringPboardType"] owner:self];
ans = [pboard setString:@(text) forType:@"NSStringPboardType"];
}
free(text);
}
return ans;
}
// Service output to be handled
- (BOOL)readSelectionFromPasteboard:(NSPasteboard *)pboard
{
NSString* text = nil;
NSArray *types = [pboard types];
if ([types containsObject:NSPasteboardTypeString] == YES) {
text = [pboard stringForType:NSPasteboardTypeString]; // public.utf8-plain-text
} else if ([types containsObject:@"NSStringPboardType"] == YES) {
text = [pboard stringForType:@"NSStringPboardType"]; // for older services (need re-encode?)
} else {
return NO;
}
if (text && [text length] > 0) {
// Terminal.app inserts the output, do the same
const char *utf8 = polymorphic_string_as_utf8(text);
if ([self hasMarkedText]) {
[self unmarkText];
debug_key("Clearing pre-edit because insertText called from readSelectionFromPasteboard\n");
GLFWkeyevent glfw_keyevent = {.ime_state = GLFW_IME_PREEDIT_CHANGED};
_glfwInputKeyboard(window, &glfw_keyevent);
}
debug_key("Sending text received in readSelectionFromPasteboard as key event\n");
GLFWkeyevent glfw_keyevent = {.text=utf8, .ime_state=GLFW_IME_COMMIT_TEXT};
_glfwInputKeyboard(window, &glfw_keyevent);
return YES;
}
return NO;
}
@end
// }}}
// GLFW window class {{{
@interface GLFWWindow : NSWindow {
_GLFWwindow* glfw_window;
}
- (instancetype)initWithGlfwWindow:(NSRect)contentRect
styleMask:(NSWindowStyleMask)style
backing:(NSBackingStoreType)backingStoreType
initWindow:(_GLFWwindow *)initWindow;
- (void) removeGLFWWindow;
@end
@implementation GLFWWindow
- (instancetype)initWithGlfwWindow:(NSRect)contentRect
styleMask:(NSWindowStyleMask)style
backing:(NSBackingStoreType)backingStoreType
initWindow:(_GLFWwindow *)initWindow
{
self = [super initWithContentRect:contentRect styleMask:style backing:backingStoreType defer:NO];
if (self != nil) {
glfw_window = initWindow;
self.tabbingMode = NSWindowTabbingModeDisallowed;
}
return self;
}
- (void) removeGLFWWindow
{
glfw_window = NULL;
}
- (BOOL)validateMenuItem:(NSMenuItem *)item {
if (item.action == @selector(performMiniaturize:)) return YES;
return [super validateMenuItem:item];
}
- (void)performMiniaturize:(id)sender
{
if (glfw_window && (!glfw_window->decorated || glfw_window->ns.titlebar_hidden)) [self miniaturize:self];
else [super performMiniaturize:sender];
}
- (BOOL)canBecomeKeyWindow
{
// Required for NSWindowStyleMaskBorderless windows
return YES;
}
- (BOOL)canBecomeMainWindow
{
return YES;
}
- (void)toggleFullScreen:(nullable id)sender
{
if (glfw_window) {
if (glfw_window->ns.in_fullscreen_transition) return;
if (glfw_window->ns.toggleFullscreenCallback && glfw_window->ns.toggleFullscreenCallback((GLFWwindow*)glfw_window) == 1) return;
glfw_window->ns.in_fullscreen_transition = true;
}
// When resizeIncrements is set, Cocoa cannot restore the original window size after returning from fullscreen.
const NSSize original = [self resizeIncrements];
[self setResizeIncrements:NSMakeSize(1.0, 1.0)];
[super toggleFullScreen:sender];
[self setResizeIncrements:original];
// When the window decoration is hidden, toggling fullscreen causes the style mask to be changed,
// and causes the first responder to be cleared.
if (glfw_window && !glfw_window->decorated && glfw_window->ns.view) [self makeFirstResponder:glfw_window->ns.view];
}
@end
// }}}
// Create the Cocoa window
//
static bool createNativeWindow(_GLFWwindow* window,
const _GLFWwndconfig* wndconfig,
const _GLFWfbconfig* fbconfig)
{
window->ns.delegate = [[GLFWWindowDelegate alloc] initWithGlfwWindow:window];
if (window->ns.delegate == nil)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Cocoa: Failed to create window delegate");
return false;
}
NSRect contentRect;
if (window->monitor)
{
GLFWvidmode mode;
int xpos, ypos;
_glfwPlatformGetVideoMode(window->monitor, &mode);
_glfwPlatformGetMonitorPos(window->monitor, &xpos, &ypos);
contentRect = NSMakeRect(xpos, ypos, mode.width, mode.height);
}
else
contentRect = NSMakeRect(0, 0, wndconfig->width, wndconfig->height);
window->ns.object = [[GLFWWindow alloc]
initWithGlfwWindow:contentRect
styleMask:getStyleMask(window)
backing:NSBackingStoreBuffered
initWindow:window
];
if (window->ns.object == nil)
{
_glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to create window");
return false;
}
if (window->monitor)
[window->ns.object setLevel:NSMainMenuWindowLevel + 1];
else
{
[(NSWindow*) window->ns.object center];
_glfw.ns.cascadePoint =
NSPointToCGPoint([window->ns.object cascadeTopLeftFromPoint:
NSPointFromCGPoint(_glfw.ns.cascadePoint)]);
if (wndconfig->resizable)
{
const NSWindowCollectionBehavior behavior =
NSWindowCollectionBehaviorFullScreenPrimary |
NSWindowCollectionBehaviorManaged;
[window->ns.object setCollectionBehavior:behavior];
}
if (wndconfig->floating)
[window->ns.object setLevel:NSFloatingWindowLevel];
if (wndconfig->maximized)
[window->ns.object zoom:nil];
}
if (strlen(wndconfig->ns.frameName))
[window->ns.object setFrameAutosaveName:@(wndconfig->ns.frameName)];
window->ns.view = [[GLFWContentView alloc] initWithGlfwWindow:window];
window->ns.retina = wndconfig->ns.retina;
if (fbconfig->transparent)
{
[window->ns.object setOpaque:NO];
[window->ns.object setHasShadow:NO];
[window->ns.object setBackgroundColor:[NSColor clearColor]];
}
[window->ns.object setContentView:window->ns.view];
[window->ns.object makeFirstResponder:window->ns.view];
[window->ns.object setTitle:@(wndconfig->title)];
[window->ns.object setDelegate:window->ns.delegate];
[window->ns.object setAcceptsMouseMovedEvents:YES];
[window->ns.object setRestorable:NO];
_glfwPlatformGetWindowSize(window, &window->ns.width, &window->ns.height);
_glfwPlatformGetFramebufferSize(window, &window->ns.fbWidth, &window->ns.fbHeight);
return true;
}
//////////////////////////////////////////////////////////////////////////
////// GLFW platform API //////
//////////////////////////////////////////////////////////////////////////
int _glfwPlatformCreateWindow(_GLFWwindow* window,
const _GLFWwndconfig* wndconfig,
const _GLFWctxconfig* ctxconfig,
const _GLFWfbconfig* fbconfig)
{
window->ns.deadKeyState = 0;
if (!_glfw.ns.finishedLaunching)
{
[NSApp run];
_glfw.ns.finishedLaunching = true;
}
if (!createNativeWindow(window, wndconfig, fbconfig))
return false;
switch((GlfwCocoaColorSpaces)wndconfig->ns.color_space) {
case SRGB_COLORSPACE: [window->ns.object setColorSpace:[NSColorSpace sRGBColorSpace]]; break;
case DISPLAY_P3_COLORSPACE: [window->ns.object setColorSpace:[NSColorSpace displayP3ColorSpace]]; break;
case DEFAULT_COLORSPACE: break;
}
if (ctxconfig->client != GLFW_NO_API)
{
if (ctxconfig->source == GLFW_NATIVE_CONTEXT_API)
{
if (!_glfwInitNSGL())
return false;
if (!_glfwCreateContextNSGL(window, ctxconfig, fbconfig))
return false;
}
else if (ctxconfig->source == GLFW_EGL_CONTEXT_API)
{
// EGL implementation on macOS use CALayer* EGLNativeWindowType so we
// need to get the layer for EGL window surface creation.
[window->ns.view setWantsLayer:YES];
window->ns.layer = [window->ns.view layer];
if (!_glfwInitEGL())
return false;
if (!_glfwCreateContextEGL(window, ctxconfig, fbconfig))
return false;
}
else if (ctxconfig->source == GLFW_OSMESA_CONTEXT_API)
{
if (!_glfwInitOSMesa())
return false;
if (!_glfwCreateContextOSMesa(window, ctxconfig, fbconfig))
return false;
}
}
if (window->monitor)
{
// Do not show the window here until after setting the window size, maximized state, and full screen
// _glfwPlatformShowWindow(window);
// _glfwPlatformFocusWindow(window);
acquireMonitor(window);
}
return true;
}
void _glfwPlatformDestroyWindow(_GLFWwindow* window)
{
if (_glfw.ns.disabledCursorWindow == window)
_glfw.ns.disabledCursorWindow = NULL;
[window->ns.object orderOut:nil];
if (window->monitor)
releaseMonitor(window);
if (window->context.destroy)
window->context.destroy(window);
[window->ns.object setDelegate:nil];
[window->ns.delegate release];
window->ns.delegate = nil;
[window->ns.view removeGLFWWindow];
[window->ns.view release];
window->ns.view = nil;
[window->ns.object removeGLFWWindow];
[window->ns.object close];
window->ns.object = nil;
}
void _glfwPlatformSetWindowTitle(_GLFWwindow* window UNUSED, const char* title)
{
NSString* string = @(title);
[window->ns.object setTitle:string];
// HACK: Set the miniwindow title explicitly as setTitle: doesn't update it
// if the window lacks NSWindowStyleMaskTitled
[window->ns.object setMiniwindowTitle:string];
}
void _glfwPlatformSetWindowIcon(_GLFWwindow* window UNUSED,
int count UNUSED, const GLFWimage* images UNUSED)
{
_glfwInputError(GLFW_FEATURE_UNAVAILABLE,
"Cocoa: Regular windows do not have icons on macOS");
}
void _glfwPlatformGetWindowPos(_GLFWwindow* window, int* xpos, int* ypos)
{
const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
if (xpos)
*xpos = (int)contentRect.origin.x;
if (ypos)
*ypos = (int)_glfwTransformYNS(contentRect.origin.y + contentRect.size.height - 1);
}
void _glfwPlatformSetWindowPos(_GLFWwindow* window, int x, int y)
{
const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
const NSRect dummyRect = NSMakeRect(x, _glfwTransformYNS(y + contentRect.size.height - 1), 0, 0);
const NSRect frameRect = [window->ns.object frameRectForContentRect:dummyRect];
[window->ns.object setFrameOrigin:frameRect.origin];
}
void _glfwPlatformGetWindowSize(_GLFWwindow* window, int* width, int* height)
{
const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
if (width)
*width = (int)contentRect.size.width;
if (height)
*height = (int)contentRect.size.height;
}
void _glfwPlatformSetWindowSize(_GLFWwindow* window, int width, int height)
{
if (window->monitor)
{
if (window->monitor->window == window)
acquireMonitor(window);
}
else
{
// Disable window resizing in fullscreen.
if ([window->ns.object styleMask] & NSWindowStyleMaskFullScreen || window->ns.in_traditional_fullscreen) return;
NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
contentRect.origin.y += contentRect.size.height - height;
contentRect.size = NSMakeSize(width, height);
[window->ns.object setFrame:[window->ns.object frameRectForContentRect:contentRect]
display:YES];
}
}
void _glfwPlatformSetWindowSizeLimits(_GLFWwindow* window,
int minwidth, int minheight,
int maxwidth, int maxheight)
{
if (minwidth == GLFW_DONT_CARE || minheight == GLFW_DONT_CARE)
[window->ns.object setContentMinSize:NSMakeSize(0, 0)];
else
[window->ns.object setContentMinSize:NSMakeSize(minwidth, minheight)];
if (maxwidth == GLFW_DONT_CARE || maxheight == GLFW_DONT_CARE)
[window->ns.object setContentMaxSize:NSMakeSize(DBL_MAX, DBL_MAX)];
else
[window->ns.object setContentMaxSize:NSMakeSize(maxwidth, maxheight)];
}
void _glfwPlatformSetWindowAspectRatio(_GLFWwindow* window, int numer, int denom)
{
if (numer == GLFW_DONT_CARE || denom == GLFW_DONT_CARE)
[window->ns.object setResizeIncrements:NSMakeSize(1.0, 1.0)];
else
[window->ns.object setContentAspectRatio:NSMakeSize(numer, denom)];
}
void _glfwPlatformSetWindowSizeIncrements(_GLFWwindow* window, int widthincr, int heightincr)
{
if (widthincr != GLFW_DONT_CARE && heightincr != GLFW_DONT_CARE) {
float xscale = 1, yscale = 1;
_glfwPlatformGetWindowContentScale(window, &xscale, &yscale);
[window->ns.object setResizeIncrements:NSMakeSize(widthincr / xscale, heightincr / yscale)];
} else {
[window->ns.object setResizeIncrements:NSMakeSize(1.0, 1.0)];
}
}
void _glfwPlatformGetFramebufferSize(_GLFWwindow* window, int* width, int* height)
{
const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
const NSRect fbRect = [window->ns.view convertRectToBacking:contentRect];
if (width)
*width = (int) fbRect.size.width;
if (height)
*height = (int) fbRect.size.height;
}
void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
int* left, int* top,
int* right, int* bottom)
{
const NSRect contentRect = get_window_size_without_border_in_logical_pixels(window);
const NSRect frameRect = [window->ns.object frameRectForContentRect:contentRect];
if (left)
*left = (int)(contentRect.origin.x - frameRect.origin.x);
if (top)
*top = (int)(frameRect.origin.y + frameRect.size.height -
contentRect.origin.y - contentRect.size.height);
if (right)
*right = (int)(frameRect.origin.x + frameRect.size.width -
contentRect.origin.x - contentRect.size.width);
if (bottom)
*bottom = (int)(contentRect.origin.y - frameRect.origin.y);
}
void _glfwPlatformGetWindowContentScale(_GLFWwindow* window,
float* xscale, float* yscale)
{
const NSRect points = get_window_size_without_border_in_logical_pixels(window);
const NSRect pixels = [window->ns.view convertRectToBacking:points];
if (xscale)
*xscale = (float) (pixels.size.width / points.size.width);
if (yscale)
*yscale = (float) (pixels.size.height / points.size.height);
}
monotonic_t _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED)
{
return s_double_to_monotonic_t([NSEvent doubleClickInterval]);
}
void _glfwPlatformIconifyWindow(_GLFWwindow* window)
{
[window->ns.object miniaturize:nil];
}
void _glfwPlatformRestoreWindow(_GLFWwindow* window)
{
if ([window->ns.object isMiniaturized])
[window->ns.object deminiaturize:nil];
else if ([window->ns.object isZoomed])
[window->ns.object zoom:nil];
}
void _glfwPlatformMaximizeWindow(_GLFWwindow* window)
{
if (![window->ns.object isZoomed]) {
const NSSize original = [window->ns.object resizeIncrements];
[window->ns.object setResizeIncrements:NSMakeSize(1.0, 1.0)];
[window->ns.object zoom:nil];
[window->ns.object setResizeIncrements:original];
}
}
void _glfwPlatformShowWindow(_GLFWwindow* window)
{
[window->ns.object orderFront:nil];
}
void _glfwPlatformHideWindow(_GLFWwindow* window)
{
[window->ns.object orderOut:nil];
}
void _glfwPlatformRequestWindowAttention(_GLFWwindow* window UNUSED)
{
[NSApp requestUserAttention:NSInformationalRequest];
}
int _glfwPlatformWindowBell(_GLFWwindow* window UNUSED)
{
NSBeep();
return true;
}
void _glfwPlatformFocusWindow(_GLFWwindow* window)
{
// Make us the active application
// HACK: This is here to prevent applications using only hidden windows from
// being activated, but should probably not be done every time any
// window is shown
[NSApp activateIgnoringOtherApps:YES];
[window->ns.object makeKeyAndOrderFront:nil];
}
void _glfwPlatformSetWindowMonitor(_GLFWwindow* window,
_GLFWmonitor* monitor,
int xpos, int ypos,
int width, int height,
int refreshRate UNUSED)
{
if (window->monitor == monitor)
{
if (monitor)
{
if (monitor->window == window)
acquireMonitor(window);
}
else
{
const NSRect contentRect =
NSMakeRect(xpos, _glfwTransformYNS(ypos + height - 1), width, height);
const NSRect frameRect =
[window->ns.object frameRectForContentRect:contentRect
styleMask:getStyleMask(window)];
[window->ns.object setFrame:frameRect display:YES];
}
return;
}
if (window->monitor)
releaseMonitor(window);
_glfwInputWindowMonitor(window, monitor);
const NSUInteger styleMask = getStyleMask(window);
[window->ns.object setStyleMask:styleMask];
// HACK: Changing the style mask can cause the first responder to be cleared
[window->ns.object makeFirstResponder:window->ns.view];
if (window->monitor)
{
[window->ns.object setLevel:NSMainMenuWindowLevel + 1];
[window->ns.object setHasShadow:NO];
acquireMonitor(window);
}
else
{
NSRect contentRect = NSMakeRect(xpos, _glfwTransformYNS(ypos + height - 1),
width, height);
NSRect frameRect = [window->ns.object frameRectForContentRect:contentRect
styleMask:styleMask];
[window->ns.object setFrame:frameRect display:YES];
if (window->numer != GLFW_DONT_CARE &&
window->denom != GLFW_DONT_CARE)
{
[window->ns.object setContentAspectRatio:NSMakeSize(window->numer,
window->denom)];
}
if (window->minwidth != GLFW_DONT_CARE &&
window->minheight != GLFW_DONT_CARE)
{
[window->ns.object setContentMinSize:NSMakeSize(window->minwidth,
window->minheight)];
}
if (window->maxwidth != GLFW_DONT_CARE &&
window->maxheight != GLFW_DONT_CARE)
{
[window->ns.object setContentMaxSize:NSMakeSize(window->maxwidth,
window->maxheight)];
}
if (window->floating)
[window->ns.object setLevel:NSFloatingWindowLevel];
else
[window->ns.object setLevel:NSNormalWindowLevel];
[window->ns.object setHasShadow:YES];
// HACK: Clearing NSWindowStyleMaskTitled resets and disables the window
// title property but the miniwindow title property is unaffected
[window->ns.object setTitle:[window->ns.object miniwindowTitle]];
}
}
int _glfwPlatformWindowFocused(_GLFWwindow* window)
{
return [window->ns.object isKeyWindow];
}
int _glfwPlatformWindowOccluded(_GLFWwindow* window)
{
return !([window->ns.object occlusionState] & NSWindowOcclusionStateVisible);
}
int _glfwPlatformWindowIconified(_GLFWwindow* window)
{
return [window->ns.object isMiniaturized];
}
int _glfwPlatformWindowVisible(_GLFWwindow* window)
{
return [window->ns.object isVisible];
}
int _glfwPlatformWindowMaximized(_GLFWwindow* window)
{
return [window->ns.object isZoomed];
}
int _glfwPlatformWindowHovered(_GLFWwindow* window)
{
const NSPoint point = [NSEvent mouseLocation];
if ([NSWindow windowNumberAtPoint:point belowWindowWithWindowNumber:0] !=
[window->ns.object windowNumber])
{
return false;
}
return NSMouseInRect(point,
[window->ns.object convertRectToScreen:[window->ns.view frame]], NO);
}
int _glfwPlatformFramebufferTransparent(_GLFWwindow* window)
{
return ![window->ns.object isOpaque] && ![window->ns.view isOpaque];
}
void _glfwPlatformSetWindowResizable(_GLFWwindow* window, bool enabled UNUSED)
{
[window->ns.object setStyleMask:getStyleMask(window)];
}
void _glfwPlatformSetWindowDecorated(_GLFWwindow* window, bool enabled UNUSED)
{
[window->ns.object setStyleMask:getStyleMask(window)];
[window->ns.object makeFirstResponder:window->ns.view];
}
void _glfwPlatformSetWindowFloating(_GLFWwindow* window, bool enabled)
{
if (enabled)
[window->ns.object setLevel:NSFloatingWindowLevel];
else
[window->ns.object setLevel:NSNormalWindowLevel];
}
void _glfwPlatformSetWindowMousePassthrough(_GLFWwindow* window, bool enabled)
{
[window->ns.object setIgnoresMouseEvents:enabled];
}
float _glfwPlatformGetWindowOpacity(_GLFWwindow* window)
{
return (float) [window->ns.object alphaValue];
}
void _glfwPlatformSetWindowOpacity(_GLFWwindow* window, float opacity)
{
[window->ns.object setAlphaValue:opacity];
}
void _glfwPlatformSetRawMouseMotion(_GLFWwindow *window UNUSED, bool enabled UNUSED)
{
_glfwInputError(GLFW_FEATURE_UNIMPLEMENTED,
"Cocoa: Raw mouse motion not yet implemented");
}
bool _glfwPlatformRawMouseMotionSupported(void)
{
return false;
}
void
_glfwDispatchRenderFrame(CGDirectDisplayID displayID) {
_GLFWwindow *w = _glfw.windowListHead;
while (w) {
if (w->ns.renderFrameRequested && displayID == displayIDForWindow(w)) {
w->ns.renderFrameRequested = false;
w->ns.renderFrameCallback((GLFWwindow*)w);
}
w = w->next;
}
for (size_t i = 0; i < _glfw.ns.displayLinks.count; i++) {
_GLFWDisplayLinkNS *dl = &_glfw.ns.displayLinks.entries[i];
if (dl->displayID == displayID) {
dl->first_unserviced_render_frame_request_at = 0;
}
}
}
void _glfwPlatformGetCursorPos(_GLFWwindow* window, double* xpos, double* ypos)
{
const NSRect contentRect = [window->ns.view frame];
// NOTE: The returned location uses base 0,1 not 0,0
const NSPoint pos = [window->ns.object mouseLocationOutsideOfEventStream];
if (xpos)
*xpos = pos.x;
if (ypos)
*ypos = contentRect.size.height - pos.y;
}
void _glfwPlatformSetCursorPos(_GLFWwindow* window, double x, double y)
{
updateCursorImage(window);
const NSRect contentRect = [window->ns.view frame];
// NOTE: The returned location uses base 0,1 not 0,0
const NSPoint pos = [window->ns.object mouseLocationOutsideOfEventStream];
window->ns.cursorWarpDeltaX += x - pos.x;
window->ns.cursorWarpDeltaY += y - contentRect.size.height + pos.y;
if (window->monitor)
{
CGDisplayMoveCursorToPoint(window->monitor->ns.displayID,
CGPointMake(x, y));
}
else
{
const NSRect localRect = NSMakeRect(x, contentRect.size.height - y - 1, 0, 0);
const NSRect globalRect = [window->ns.object convertRectToScreen:localRect];
const NSPoint globalPoint = globalRect.origin;
CGWarpMouseCursorPosition(CGPointMake(globalPoint.x,
_glfwTransformYNS(globalPoint.y)));
}
}
void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode UNUSED)
{
if (_glfwPlatformWindowFocused(window))
updateCursorMode(window);
}
const char* _glfwPlatformGetNativeKeyName(int keycode)
{
UInt32 deadKeyState = 0;
UniChar characters[8];
UniCharCount characterCount = 0;
if (UCKeyTranslate([(NSData*) _glfw.ns.unicodeData bytes],
keycode,
kUCKeyActionDisplay,
0,
LMGetKbdType(),
kUCKeyTranslateNoDeadKeysBit,
&deadKeyState,
sizeof(characters) / sizeof(characters[0]),
&characterCount,
characters) != noErr)
{
return NULL;
}
if (!characterCount)
return NULL;
convert_utf16_to_utf8(characters, characterCount, _glfw.ns.keyName, sizeof(_glfw.ns.keyName));
return _glfw.ns.keyName;
}
int _glfwPlatformGetNativeKeyForKey(uint32_t glfw_key)
{
if (GLFW_FKEY_FIRST <= glfw_key && glfw_key <= GLFW_FKEY_LAST) { // {{{
switch(glfw_key) {
/* start functional to macu (auto generated by gen-key-constants.py do not edit) */
case GLFW_FKEY_ENTER: return NSCarriageReturnCharacter;
case GLFW_FKEY_TAB: return NSTabCharacter;
case GLFW_FKEY_BACKSPACE: return NSBackspaceCharacter;
case GLFW_FKEY_INSERT: return NSInsertFunctionKey;
case GLFW_FKEY_DELETE: return NSDeleteFunctionKey;
case GLFW_FKEY_LEFT: return NSLeftArrowFunctionKey;
case GLFW_FKEY_RIGHT: return NSRightArrowFunctionKey;
case GLFW_FKEY_UP: return NSUpArrowFunctionKey;
case GLFW_FKEY_DOWN: return NSDownArrowFunctionKey;
case GLFW_FKEY_PAGE_UP: return NSPageUpFunctionKey;
case GLFW_FKEY_PAGE_DOWN: return NSPageDownFunctionKey;
case GLFW_FKEY_HOME: return NSHomeFunctionKey;
case GLFW_FKEY_END: return NSEndFunctionKey;
case GLFW_FKEY_SCROLL_LOCK: return NSScrollLockFunctionKey;
case GLFW_FKEY_NUM_LOCK: return NSClearLineFunctionKey;
case GLFW_FKEY_PRINT_SCREEN: return NSPrintScreenFunctionKey;
case GLFW_FKEY_PAUSE: return NSPauseFunctionKey;
case GLFW_FKEY_MENU: return NSMenuFunctionKey;
case GLFW_FKEY_F1: return NSF1FunctionKey;
case GLFW_FKEY_F2: return NSF2FunctionKey;
case GLFW_FKEY_F3: return NSF3FunctionKey;
case GLFW_FKEY_F4: return NSF4FunctionKey;
case GLFW_FKEY_F5: return NSF5FunctionKey;
case GLFW_FKEY_F6: return NSF6FunctionKey;
case GLFW_FKEY_F7: return NSF7FunctionKey;
case GLFW_FKEY_F8: return NSF8FunctionKey;
case GLFW_FKEY_F9: return NSF9FunctionKey;
case GLFW_FKEY_F10: return NSF10FunctionKey;
case GLFW_FKEY_F11: return NSF11FunctionKey;
case GLFW_FKEY_F12: return NSF12FunctionKey;
case GLFW_FKEY_F13: return NSF13FunctionKey;
case GLFW_FKEY_F14: return NSF14FunctionKey;
case GLFW_FKEY_F15: return NSF15FunctionKey;
case GLFW_FKEY_F16: return NSF16FunctionKey;
case GLFW_FKEY_F17: return NSF17FunctionKey;
case GLFW_FKEY_F18: return NSF18FunctionKey;
case GLFW_FKEY_F19: return NSF19FunctionKey;
case GLFW_FKEY_F20: return NSF20FunctionKey;
case GLFW_FKEY_F21: return NSF21FunctionKey;
case GLFW_FKEY_F22: return NSF22FunctionKey;
case GLFW_FKEY_F23: return NSF23FunctionKey;
case GLFW_FKEY_F24: return NSF24FunctionKey;
case GLFW_FKEY_F25: return NSF25FunctionKey;
case GLFW_FKEY_F26: return NSF26FunctionKey;
case GLFW_FKEY_F27: return NSF27FunctionKey;
case GLFW_FKEY_F28: return NSF28FunctionKey;
case GLFW_FKEY_F29: return NSF29FunctionKey;
case GLFW_FKEY_F30: return NSF30FunctionKey;
case GLFW_FKEY_F31: return NSF31FunctionKey;
case GLFW_FKEY_F32: return NSF32FunctionKey;
case GLFW_FKEY_F33: return NSF33FunctionKey;
case GLFW_FKEY_F34: return NSF34FunctionKey;
case GLFW_FKEY_F35: return NSF35FunctionKey;
case GLFW_FKEY_KP_ENTER: return NSEnterCharacter;
/* end functional to macu */
default:
return 0;
}
} // }}}
if (!is_pua_char(glfw_key)) return glfw_key;
return 0;
}
int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
const GLFWimage* image,
int xhot, int yhot, int count)
{
NSImage* native;
NSBitmapImageRep* rep;
native = [[NSImage alloc] initWithSize:NSMakeSize(image->width, image->height)];
if (native == nil)
return false;
for (int i = 0; i < count; i++) {
const GLFWimage *src = image + i;
rep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:src->width
pixelsHigh:src->height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSCalibratedRGBColorSpace
bitmapFormat:NSBitmapFormatAlphaNonpremultiplied
bytesPerRow:src->width * 4
bitsPerPixel:32];
if (rep == nil) {
[native release];
return false;
}
memcpy([rep bitmapData], src->pixels, src->width * src->height * 4);
[native addRepresentation:rep];
[rep release];
}
cursor->ns.object = [[NSCursor alloc] initWithImage:native
hotSpot:NSMakePoint(xhot, yhot)];
[native release];
if (cursor->ns.object == nil)
return false;
return true;
}
int _glfwPlatformCreateStandardCursor(_GLFWcursor* cursor, GLFWCursorShape shape)
{
#define C(name, val) case name: cursor->ns.object = [NSCursor val]; break;
#define U(name, val) case name: cursor->ns.object = [[NSCursor class] performSelector:@selector(val)]; break;
switch(shape) {
C(GLFW_ARROW_CURSOR, arrowCursor);
C(GLFW_IBEAM_CURSOR, IBeamCursor);
C(GLFW_CROSSHAIR_CURSOR, crosshairCursor);
C(GLFW_HAND_CURSOR, pointingHandCursor);
C(GLFW_HRESIZE_CURSOR, resizeLeftRightCursor);
C(GLFW_VRESIZE_CURSOR, resizeUpDownCursor);
U(GLFW_NW_RESIZE_CURSOR, _windowResizeNorthWestSouthEastCursor);
U(GLFW_NE_RESIZE_CURSOR, _windowResizeNorthEastSouthWestCursor);
U(GLFW_SW_RESIZE_CURSOR, _windowResizeNorthEastSouthWestCursor);
U(GLFW_SE_RESIZE_CURSOR, _windowResizeNorthWestSouthEastCursor);
case GLFW_INVALID_CURSOR:
return false;
}
#undef C
#undef U
if (!cursor->ns.object)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Cocoa: Failed to retrieve standard cursor");
return false;
}
[cursor->ns.object retain];
return true;
}
void _glfwPlatformDestroyCursor(_GLFWcursor* cursor)
{
if (cursor->ns.object)
[(NSCursor*) cursor->ns.object release];
}
void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor UNUSED)
{
if (cursorInContentArea(window))
updateCursorImage(window);
}
bool _glfwPlatformIsFullscreen(_GLFWwindow* w, unsigned int flags) {
NSWindow *window = w->ns.object;
bool traditional = !(flags & 1);
if (traditional) { if(@available(macOS 10.15.7, *)) return w->ns.in_traditional_fullscreen; }
NSWindowStyleMask sm = [window styleMask];
return sm & NSWindowStyleMaskFullScreen;
}
bool _glfwPlatformToggleFullscreen(_GLFWwindow* w, unsigned int flags) {
NSWindow *window = w->ns.object;
bool made_fullscreen = true;
bool traditional = !(flags & 1);
NSWindowStyleMask sm = [window styleMask];
if (traditional) {
if (@available(macOS 10.15.7, *)) {
// As of Big Turd NSWindowStyleMaskFullScreen is no longer useable
// Also no longer compatible after a minor release of macOS 10.15.7
if (!w->ns.in_traditional_fullscreen) {
w->ns.pre_full_screen_style_mask = sm;
[window setStyleMask: NSWindowStyleMaskBorderless];
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock];
[window setFrame:[window.screen frame] display:YES];
w->ns.in_traditional_fullscreen = true;
} else {
made_fullscreen = false;
[window setStyleMask: w->ns.pre_full_screen_style_mask];
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault];
w->ns.in_traditional_fullscreen = false;
}
} else {
bool in_fullscreen = sm & NSWindowStyleMaskFullScreen;
if (!(in_fullscreen)) {
sm |= NSWindowStyleMaskBorderless | NSWindowStyleMaskFullScreen;
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock];
} else {
made_fullscreen = false;
sm &= ~(NSWindowStyleMaskBorderless | NSWindowStyleMaskFullScreen);
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault];
}
[window setStyleMask: sm];
}
// Changing the style mask causes the first responder to be cleared
[window makeFirstResponder:w->ns.view];
// If the dock and menubar are hidden going from maximized to fullscreen doesnt change the window size
// and macOS forgets to trigger windowDidResize, so call it ourselves
NSNotification *notification = [NSNotification notificationWithName:NSWindowDidResizeNotification object:window];
[w->ns.delegate performSelector:@selector(windowDidResize:) withObject:notification afterDelay:0];
} else {
bool in_fullscreen = sm & NSWindowStyleMaskFullScreen;
if (in_fullscreen) made_fullscreen = false;
[window toggleFullScreen: nil];
}
// Update window button visibility
if (w->ns.titlebar_hidden) {
// The hidden buttons might be automatically reset to be visible after going full screen
// to show up in the auto-hide title bar, so they need to be set back to hidden.
BOOL button_hidden = YES;
// When title bar is configured to be hidden, it should be shown with buttons (auto-hide) after going to full screen.
if (!traditional) {
button_hidden = (BOOL) !made_fullscreen;
}
[[window standardWindowButton: NSWindowCloseButton] setHidden:button_hidden];
[[window standardWindowButton: NSWindowMiniaturizeButton] setHidden:button_hidden];
[[window standardWindowButton: NSWindowZoomButton] setHidden:button_hidden];
}
return made_fullscreen;
}
// Clipboard {{{
#define UTI_ROUNDTRIP_PREFIX @"uti-is-typical-apple-nih."
static NSString*
mime_to_uti(const char *mime) {
if (strcmp(mime, "text/plain") == 0) return NSPasteboardTypeString;
if (@available(macOS 11.0, *)) {
UTType *t = [UTType typeWithMIMEType:@(mime)]; // auto-released
if (t != nil && !t.dynamic) return t.identifier;
}
return [NSString stringWithFormat:@"%@%s", UTI_ROUNDTRIP_PREFIX, mime]; // auto-released
}
static const char*
uti_to_mime(NSString *uti) {
if ([uti isEqualToString:NSPasteboardTypeString]) return "text/plain";
if ([uti hasPrefix:UTI_ROUNDTRIP_PREFIX]) return [[uti substringFromIndex:[UTI_ROUNDTRIP_PREFIX length]] UTF8String];
if (@available(macOS 11.0, *)) {
UTType *t = [UTType typeWithIdentifier:uti]; // auto-released
if (t.preferredMIMEType != nil) return [t.preferredMIMEType UTF8String];
}
return "";
}
static void
list_clipboard_mimetypes(GLFWclipboardwritedatafun write_data, void *object) {
#define w(x) { if (ok) ok = write_data(object, x, strlen(x)); }
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
BOOL has_file_urls = [pasteboard canReadObjectForClasses:@[[NSURL class]] options:options];
BOOL has_strings = [pasteboard canReadObjectForClasses:@[[NSString class]] options:nil];
/* NSLog(@"has_file_urls: %d has_strings: %d", has_file_urls, has_strings); */
bool ok = true;
if (has_strings) w("text/plain");
if (has_file_urls) w("text/local-path-list");
for (NSPasteboardItem * item in pasteboard.pasteboardItems) {
for (NSPasteboardType type in item.types) {
/* NSLog(@"%@", type); */
const char *mime = uti_to_mime(type);
if (mime && mime[0] && ![@(mime) hasPrefix:@"text/plain"]) {
/* NSLog(@"ut: %@ mt: %@ tags: %@", ut, ut.preferredMIMEType, ut.tags); */
w(mime);
}
}
}
#undef w
}
static void
get_text_plain(GLFWclipboardwritedatafun write_data, void *object) {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
NSArray* objs = [pasteboard readObjectsForClasses:@[[NSURL class], [NSString class]] options:options];
bool found = false;
if (objs) {
const NSUInteger count = [objs count];
if (count) {
NSMutableData *path_list = [NSMutableData dataWithCapacity:4096]; // auto-released
NSMutableData *text_list = [NSMutableData dataWithCapacity:4096]; // auto-released
for (NSUInteger i = 0; i < count; i++) {
id obj = objs[i];
if ([obj isKindOfClass:[NSURL class]]) {
NSURL *url = (NSURL*)obj;
if (url.fileURL && url.fileSystemRepresentation) {
if ([path_list length] > 0) [path_list appendBytes:"\n" length:1];
[path_list appendBytes:url.fileSystemRepresentation length:strlen(url.fileSystemRepresentation)];
}
} else if ([obj isKindOfClass:[NSString class]]) {
if ([text_list length] > 0) [text_list appendBytes:"\n" length:1];
[text_list appendData:[obj dataUsingEncoding:NSUTF8StringEncoding]];
}
}
const NSMutableData *text = nil;
if (path_list.length > 0) text = path_list;
else if (text_list.length > 0) text = text_list;
if (text) {
found = true;
write_data(object, text.mutableBytes, text.length);
}
}
}
if (!found) _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to retrieve text/plain from pasteboard");
}
void
_glfwPlatformGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object) {
if (clipboard_type != GLFW_CLIPBOARD) return;
if (mime_type == NULL) {
list_clipboard_mimetypes(write_data, object);
return;
}
if (strcmp(mime_type, "text/plain") == 0) {
get_text_plain(write_data, object);
return;
}
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
/* NSLog(@"mime: %s uti: %@", mime_type, mime_to_uti(mime_type)); */
NSPasteboardType t = [pasteboard availableTypeFromArray:@[mime_to_uti(mime_type)]];
/* NSLog(@"available type: %@", t); */
if (t != nil) {
NSData *data = [pasteboard dataForType:t]; // auto-released
/* NSLog(@"data: %@", data); */
if (data != nil && data.length > 0) {
write_data(object, data.bytes, data.length);
}
}
}
static NSMutableData*
get_clipboard_data(const _GLFWClipboardData *cd, const char *mime) {
NSMutableData *ans = [NSMutableData dataWithCapacity:8192];
if (ans == nil) return nil;
GLFWDataChunk chunk = cd->get_data(mime, NULL, cd->ctype);
void *iter = chunk.iter;
if (!iter) return ans;
while (true) {
chunk = cd->get_data(mime, iter, cd->ctype);
if (!chunk.sz) break;
[ans appendBytes:chunk.data length:chunk.sz];
if (chunk.free) chunk.free((void*)chunk.free_data);
}
cd->get_data(NULL, iter, cd->ctype);
return ans;
}
void
_glfwPlatformSetClipboard(GLFWClipboardType t) {
if (t != GLFW_CLIPBOARD) return;
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
NSMutableArray<NSPasteboardType> *ptypes = [NSMutableArray arrayWithCapacity:_glfw.clipboard.num_mime_types]; // auto-released
for (size_t i = 0; i < _glfw.clipboard.num_mime_types; i++) {
[ptypes addObject:mime_to_uti(_glfw.clipboard.mime_types[i])];
}
[pasteboard declareTypes:ptypes owner:nil];
for (size_t i = 0; i < _glfw.clipboard.num_mime_types; i++) {
NSMutableData *data = get_clipboard_data(&_glfw.clipboard, _glfw.clipboard.mime_types[i]); // auto-released
/* NSLog(@"putting data: %@ for: %s with UTI: %@", data, _glfw.clipboard.mime_types[i], ptypes[i]); */
if (data != nil) [pasteboard setData:data forType:ptypes[i]];
}
}
// }}}
EGLenum _glfwPlatformGetEGLPlatform(EGLint** attribs)
{
if (_glfw.egl.ANGLE_platform_angle)
{
int type = 0;
if (_glfw.egl.ANGLE_platform_angle_opengl)
{
if (_glfw.hints.init.angleType == GLFW_ANGLE_PLATFORM_TYPE_OPENGL)
type = EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE;
}
if (_glfw.egl.ANGLE_platform_angle_metal)
{
if (_glfw.hints.init.angleType == GLFW_ANGLE_PLATFORM_TYPE_METAL)
type = EGL_PLATFORM_ANGLE_TYPE_METAL_ANGLE;
}
if (type)
{
*attribs = calloc(3, sizeof(EGLint));
(*attribs)[0] = EGL_PLATFORM_ANGLE_TYPE_ANGLE;
(*attribs)[1] = type;
(*attribs)[2] = EGL_NONE;
return EGL_PLATFORM_ANGLE_ANGLE;
}
}
return 0;
}
EGLNativeDisplayType _glfwPlatformGetEGLNativeDisplay(void)
{
return EGL_DEFAULT_DISPLAY;
}
EGLNativeWindowType _glfwPlatformGetEGLNativeWindow(_GLFWwindow* window)
{
return window->ns.layer;
}
void _glfwPlatformGetRequiredInstanceExtensions(char** extensions)
{
if (_glfw.vk.KHR_surface && _glfw.vk.EXT_metal_surface)
{
extensions[0] = "VK_KHR_surface";
extensions[1] = "VK_EXT_metal_surface";
}
else if (_glfw.vk.KHR_surface && _glfw.vk.MVK_macos_surface)
{
extensions[0] = "VK_KHR_surface";
extensions[1] = "VK_MVK_macos_surface";
}
}
int _glfwPlatformGetPhysicalDevicePresentationSupport(VkInstance instance UNUSED,
VkPhysicalDevice device UNUSED,
uint32_t queuefamily UNUSED)
{
return true;
}
VkResult _glfwPlatformCreateWindowSurface(VkInstance instance,
_GLFWwindow* window,
const VkAllocationCallbacks* allocator,
VkSurfaceKHR* surface)
{
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101100
// HACK: Dynamically load Core Animation to avoid adding an extra
// dependency for the majority who don't use MoltenVK
NSBundle* bundle = [NSBundle bundleWithPath:@"/System/Library/Frameworks/QuartzCore.framework"];
if (!bundle)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Cocoa: Failed to find QuartzCore.framework");
return VK_ERROR_EXTENSION_NOT_PRESENT;
}
// NOTE: Create the layer here as makeBackingLayer should not return nil
window->ns.layer = [[bundle classNamed:@"CAMetalLayer"] layer];
if (!window->ns.layer)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Cocoa: Failed to create layer for view");
return VK_ERROR_EXTENSION_NOT_PRESENT;
}
if (window->ns.retina)
[window->ns.layer setContentsScale:[window->ns.object backingScaleFactor]];
[window->ns.view setLayer:window->ns.layer];
[window->ns.view setWantsLayer:YES];
VkResult err;
if (_glfw.vk.EXT_metal_surface)
{
VkMetalSurfaceCreateInfoEXT sci;
PFN_vkCreateMetalSurfaceEXT vkCreateMetalSurfaceEXT;
vkCreateMetalSurfaceEXT = (PFN_vkCreateMetalSurfaceEXT)
vkGetInstanceProcAddr(instance, "vkCreateMetalSurfaceEXT");
if (!vkCreateMetalSurfaceEXT)
{
_glfwInputError(GLFW_API_UNAVAILABLE,
"Cocoa: Vulkan instance missing VK_EXT_metal_surface extension");
return VK_ERROR_EXTENSION_NOT_PRESENT;
}
memset(&sci, 0, sizeof(sci));
sci.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT;
sci.pLayer = window->ns.layer;
err = vkCreateMetalSurfaceEXT(instance, &sci, allocator, surface);
}
else
{
VkMacOSSurfaceCreateInfoMVK sci;
PFN_vkCreateMacOSSurfaceMVK vkCreateMacOSSurfaceMVK;
vkCreateMacOSSurfaceMVK = (PFN_vkCreateMacOSSurfaceMVK)
vkGetInstanceProcAddr(instance, "vkCreateMacOSSurfaceMVK");
if (!vkCreateMacOSSurfaceMVK)
{
_glfwInputError(GLFW_API_UNAVAILABLE,
"Cocoa: Vulkan instance missing VK_MVK_macos_surface extension");
return VK_ERROR_EXTENSION_NOT_PRESENT;
}
memset(&sci, 0, sizeof(sci));
sci.sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK;
sci.pView = window->ns.view;
err = vkCreateMacOSSurfaceMVK(instance, &sci, allocator, surface);
}
if (err)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Cocoa: Failed to create Vulkan surface: %s",
_glfwGetVulkanResultString(err));
}
return err;
#else
return VK_ERROR_EXTENSION_NOT_PRESENT;
#endif
}
//////////////////////////////////////////////////////////////////////////
////// GLFW native API //////
//////////////////////////////////////////////////////////////////////////
GLFWAPI id glfwGetCocoaWindow(GLFWwindow* handle)
{
_GLFWwindow* window = (_GLFWwindow*) handle;
_GLFW_REQUIRE_INIT_OR_RETURN(nil);
return window->ns.object;
}
GLFWAPI void glfwHideCocoaTitlebar(GLFWwindow* handle, bool yes) {
@autoreleasepool {
_GLFWwindow* w = (_GLFWwindow*) handle;
NSWindow *window = w->ns.object;
w->ns.titlebar_hidden = yes;
NSButton *button;
button = [window standardWindowButton: NSWindowCloseButton];
if (button) button.hidden = yes;
button = [window standardWindowButton: NSWindowMiniaturizeButton];
if (button) button.hidden = yes;
button = [window standardWindowButton: NSWindowZoomButton];
[window setTitlebarAppearsTransparent:yes];
if (button) button.hidden = yes;
if (yes) {
[window setTitleVisibility:NSWindowTitleHidden];
[window setStyleMask: [window styleMask] | NSWindowStyleMaskFullSizeContentView];
} else {
[window setTitleVisibility:NSWindowTitleVisible];
[window setStyleMask: [window styleMask] & ~NSWindowStyleMaskFullSizeContentView];
}
} // autoreleasepool
}
GLFWAPI GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow *handle, GLFWcocoatextinputfilterfun callback) {
_GLFWwindow* window = (_GLFWwindow*) handle;
_GLFW_REQUIRE_INIT_OR_RETURN(nil);
GLFWcocoatextinputfilterfun previous = window->ns.textInputFilterCallback;
window->ns.textInputFilterCallback = callback;
return previous;
}
GLFWAPI GLFWhandleurlopen glfwSetCocoaURLOpenCallback(GLFWhandleurlopen callback) {
_GLFW_REQUIRE_INIT_OR_RETURN(nil);
GLFWhandleurlopen prev = _glfw.ns.url_open_callback;
_glfw.ns.url_open_callback = callback;
return prev;
}
GLFWAPI GLFWcocoatogglefullscreenfun glfwSetCocoaToggleFullscreenIntercept(GLFWwindow *handle, GLFWcocoatogglefullscreenfun callback) {
_GLFWwindow* window = (_GLFWwindow*) handle;
_GLFW_REQUIRE_INIT_OR_RETURN(nil);
GLFWcocoatogglefullscreenfun previous = window->ns.toggleFullscreenCallback;
window->ns.toggleFullscreenCallback = callback;
return previous;
}
GLFWAPI void glfwCocoaRequestRenderFrame(GLFWwindow *w, GLFWcocoarenderframefun callback) {
requestRenderFrame((_GLFWwindow*)w, callback);
}
GLFWAPI uint32_t
glfwGetCocoaKeyEquivalent(uint32_t glfw_key, int glfw_mods, int *cocoa_mods) {
*cocoa_mods = 0;
if (glfw_mods & GLFW_MOD_SHIFT)
*cocoa_mods |= NSEventModifierFlagShift;
if (glfw_mods & GLFW_MOD_CONTROL)
*cocoa_mods |= NSEventModifierFlagControl;
if (glfw_mods & GLFW_MOD_ALT)
*cocoa_mods |= NSEventModifierFlagOption;
if (glfw_mods & GLFW_MOD_SUPER)
*cocoa_mods |= NSEventModifierFlagCommand;
if (glfw_mods & GLFW_MOD_CAPS_LOCK)
*cocoa_mods |= NSEventModifierFlagCapsLock;
return _glfwPlatformGetNativeKeyForKey(glfw_key);
}
//////////////////////////////////////////////////////////////////////////
////// GLFW internal API //////
//////////////////////////////////////////////////////////////////////////
// Transforms a y-coordinate between the CG display and NS screen spaces
//
float _glfwTransformYNS(float y)
{
return CGDisplayBounds(CGMainDisplayID()).size.height - y - 1;
}
void _glfwCocoaPostEmptyEvent(void) {
NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSMakePoint(0, 0)
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:event atStart:YES];
}