mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 08:26:56 +00:00
4612 lines
180 KiB
Objective-C
4612 lines
180 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 "../kitty/monotonic.h"
|
|
#include "glfw3.h"
|
|
#include "internal.h"
|
|
|
|
#include <Availability.h>
|
|
#import <CoreServices/CoreServices.h>
|
|
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
|
#include <errno.h>
|
|
#include <float.h>
|
|
#include <string.h>
|
|
#include <assert.h>
|
|
|
|
#define debug debug_rendering
|
|
|
|
#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;
|
|
UTType *t = [UTType typeWithMIMEType:@(mime)]; // auto-released
|
|
if (t != nil && !t.dynamic) return t.identifier;
|
|
size_t sz = strlen(mime);
|
|
NSMutableString *hex = [NSMutableString stringWithCapacity:sz * 2];
|
|
for (NSUInteger i = 0; i < sz; i++) [hex appendFormat:@"%02x", (unsigned char)mime[i]];
|
|
return [NSString stringWithFormat:@"%@%@", UTI_ROUNDTRIP_PREFIX, hex]; // auto-released
|
|
}
|
|
|
|
static char
|
|
hexval(char c, bool *ok) {
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
*ok = false;
|
|
return 0;
|
|
}
|
|
|
|
static const char*
|
|
uti_to_mime(NSString *uti) {
|
|
if ([uti isEqualToString:NSPasteboardTypeString]) return "text/plain";
|
|
if ([uti hasPrefix:UTI_ROUNDTRIP_PREFIX]) {
|
|
NSString *hexPart = [uti substringFromIndex:UTI_ROUNDTRIP_PREFIX.length];
|
|
NSUInteger hexLen = hexPart.length;
|
|
if (hexLen == 0 || hexLen % 2 != 0) { return ""; }
|
|
const char *hex = [hexPart UTF8String];
|
|
static char buf[4096];
|
|
size_t i, j;
|
|
for (i = 0, j = 0; i < hexLen && j < sizeof(buf)-1; i += 2, j++) {
|
|
char hi = hex[i], lo = hex[i + 1];
|
|
bool ok = true;
|
|
buf[j] = (hexval(hi, &ok) << 4) | hexval(lo, &ok);
|
|
if (!ok) return "";
|
|
}
|
|
buf[j] = 0;
|
|
return buf;
|
|
}
|
|
if (@available(macOS 11.0, *)) {
|
|
UTType *t = [UTType typeWithIdentifier:uti]; // auto-released
|
|
if (t.preferredMIMEType != nil) return [t.preferredMIMEType UTF8String];
|
|
}
|
|
return "";
|
|
}
|
|
|
|
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 bool
|
|
forward_dictation_selector_to_app(SEL selector, id sender) {
|
|
static SEL start_dictation_selector = NULL, stop_dictation_selector = NULL;
|
|
if (start_dictation_selector == NULL) {
|
|
start_dictation_selector = NSSelectorFromString(@"startDictation:");
|
|
stop_dictation_selector = NSSelectorFromString(@"stopDictation:");
|
|
}
|
|
if (selector != start_dictation_selector && selector != stop_dictation_selector) return false;
|
|
if ([NSApp respondsToSelector:selector]) {
|
|
debug_key("Forwarding %s to NSApp\n", [NSStringFromSelector(selector) UTF8String]);
|
|
[NSApp performSelector:selector withObject:sender];
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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->ns.titlebar_hidden) styleMask |= NSWindowStyleMaskFullSizeContentView;
|
|
|
|
if (window->monitor || !window->decorated) {
|
|
styleMask |= NSWindowStyleMaskBorderless;
|
|
} else {
|
|
styleMask |= NSWindowStyleMaskTitled | NSWindowStyleMaskClosable;
|
|
}
|
|
if (window->resizable) styleMask |= NSWindowStyleMaskResizable;
|
|
|
|
return styleMask;
|
|
}
|
|
|
|
|
|
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;
|
|
_glfwRequestRenderFrame(w);
|
|
}
|
|
|
|
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;
|
|
NSArray<NSDictionary *> *_lastScreenStates;
|
|
}
|
|
|
|
- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow;
|
|
- (void)request_delayed_cursor_update:(id)sender;
|
|
|
|
@end
|
|
|
|
static void update_titlebar_button_visibility_after_fullscreen_transition(_GLFWwindow*, bool, bool);
|
|
static void _glfwUpdateNotchCover(_GLFWwindow*);
|
|
|
|
@implementation GLFWWindowDelegate
|
|
|
|
- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow
|
|
{
|
|
self = [super init];
|
|
if (self != nil) {
|
|
window = initWindow;
|
|
_lastScreenStates = [self captureScreenStates];
|
|
window->ns.live_resize_in_progress = false;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (NSArray<NSDictionary *> *)captureScreenStates {
|
|
NSMutableArray *states = [NSMutableArray array];
|
|
for (NSScreen *screen in [NSScreen screens]) {
|
|
// Use the screen's deviceDescription, which contains a stable ID.
|
|
[states addObject:screen.deviceDescription];
|
|
}
|
|
return [states copy];
|
|
}
|
|
|
|
- (void)cleanup {
|
|
[_lastScreenStates release]; _lastScreenStates = nil;
|
|
}
|
|
|
|
- (BOOL)windowShouldClose:(id)sender
|
|
{
|
|
(void)sender;
|
|
if (window == nil) return YES;
|
|
_glfwInputWindowCloseRequest(window);
|
|
return NO;
|
|
}
|
|
|
|
- (void)windowDidResize:(NSNotification *)notification
|
|
{
|
|
(void)notification;
|
|
NSArray<NSDictionary *> *currentScreenStates = [self captureScreenStates];
|
|
const bool is_screen_change = ![_lastScreenStates isEqualToArray:currentScreenStates];
|
|
NSWindowStyleMask sm = [window->ns.object styleMask];
|
|
const bool is_fullscreen = (sm & NSWindowStyleMaskFullScreen) != 0;
|
|
NSRect frame = [window->ns.object frame];
|
|
debug_rendering(
|
|
"windowDidResize() called, is_screen_change: %d is_fullscreen: %d live_resize_in_progress: %d frame: %.1fx%.1f@(%.1f, %.1f)\n",
|
|
is_screen_change, is_fullscreen, window->ns.live_resize_in_progress, frame.size.width, frame.size.height, frame.origin.x, frame.origin.y);
|
|
if (is_screen_change) {
|
|
// This resize likely happened because a screen was added, removed, or changed resolution.
|
|
[_lastScreenStates release];
|
|
_lastScreenStates = [currentScreenStates retain];
|
|
}
|
|
[currentScreenStates release];
|
|
|
|
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);
|
|
}
|
|
// Because of a bug in macOS Tahoe we cannot redraw the window in response
|
|
// to a resize event that was caused by a screen change as the OpenGL
|
|
// context is not ready yet. See: https://github.com/kovidgoyal/kitty/issues/8983
|
|
if (window->ns.resizeCallback && !is_screen_change && !is_fullscreen && window->ns.live_resize_in_progress)
|
|
window->ns.resizeCallback((GLFWwindow*)window);
|
|
}
|
|
|
|
- (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;
|
|
if (window->ns.in_traditional_fullscreen) {
|
|
// macOS finished its Cocoa exit (cleared NSWindowStyleMaskFullScreen).
|
|
// Defer restoration to the next run loop iteration because calling
|
|
// setStyleMask: inside a delegate callback can leave the window in
|
|
// an intermediate state. setStyleMask: also triggers macOS's
|
|
// constrainFrameRect:toScreen: and window tiling logic which can
|
|
// asynchronously reposition the window, so suppress frame
|
|
// constraints during the restoration (#9572).
|
|
unsigned long long wid = window->id;
|
|
NSWindowStyleMask savedMask = window->ns.pre_full_screen_style_mask;
|
|
CGRect savedFrame = window->ns.pre_traditional_fullscreen_frame;
|
|
window->ns.in_traditional_fullscreen = false;
|
|
_glfwUpdateNotchCover(window);
|
|
window->ns.suppress_frame_constraints = true;
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
_GLFWwindow *w = NULL;
|
|
for (_GLFWwindow *ww = _glfw.windowListHead; ww; ww = ww->next) {
|
|
if (ww->id == wid) { w = ww; break; }
|
|
}
|
|
if (w) {
|
|
NSWindow *nswindow = w->ns.object;
|
|
[nswindow setStyleMask: savedMask];
|
|
[nswindow setFrame: savedFrame display:YES];
|
|
update_titlebar_button_visibility_after_fullscreen_transition(w, true, false);
|
|
[nswindow makeFirstResponder:w->ns.view];
|
|
NSNotification *resize = [NSNotification notificationWithName:NSWindowDidResizeNotification object:nswindow];
|
|
[w->ns.delegate performSelector:@selector(windowDidResize:) withObject:resize afterDelay:0];
|
|
}
|
|
// Lift the constraint guard after a delay, even if the window
|
|
// was not found (destroyed), to keep the flag consistent (#9572).
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(500 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
|
|
_GLFWwindow *w2 = NULL;
|
|
for (_GLFWwindow *ww = _glfw.windowListHead; ww; ww = ww->next) {
|
|
if (ww->id == wid) { w2 = ww; break; }
|
|
}
|
|
if (w2) w2->ns.suppress_frame_constraints = 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]);
|
|
if (forward_dictation_selector_to_app(selector, nil)) return;
|
|
}
|
|
@end // }}}
|
|
|
|
// File Promise Provider Delegate for async drag data {{{
|
|
|
|
// Structure to hold async drag state
|
|
@interface GLFWFilePromiseProviderDelegate : NSObject <NSFilePromiseProviderDelegate>
|
|
{
|
|
GLFWid windowId, instanceId;
|
|
char* mimeType; // MIME type for this provider
|
|
NSFileHandle *file_handle;
|
|
NSURL *file_url;
|
|
void (^completion_handler)(NSError*);
|
|
}
|
|
|
|
- (instancetype)initWithWindow:(_GLFWwindow*)initWindow mimeType:(const char*)mime instanceId:(GLFWid)iid;
|
|
- (void)request_drag_data;
|
|
- (void)end_transfer:(int)errorCode;
|
|
- (void)end_transfer_with_error:(NSError*)err;
|
|
- (bool)is_mimetype:(const char*)mime_type;
|
|
@end
|
|
|
|
@interface GLFWDraggingSource : NSObject <NSDraggingSource> {
|
|
NSPoint start_point, current_point;
|
|
}
|
|
@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;
|
|
GLFWDraggingSource *dragging_source;
|
|
}
|
|
|
|
- (void) removeGLFWWindow;
|
|
- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow;
|
|
- (GLFWDraggingSource*)draggingSource;
|
|
@end
|
|
|
|
@implementation GLFWContentView
|
|
|
|
- (instancetype)initWithGlfwWindow:(_GLFWwindow *)initWindow
|
|
{
|
|
self = [super init];
|
|
if (self != nil)
|
|
{
|
|
window = initWindow;
|
|
trackingArea = nil;
|
|
input_context = [[GLFWTextInputContext alloc] initWithClient:self];
|
|
dragging_source = [[GLFWDraggingSource alloc] init];
|
|
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.identifier = @"kitty-content-view";
|
|
|
|
[self updateTrackingAreas];
|
|
char tab_mime[64], window_mime[64];
|
|
snprintf(tab_mime, sizeof(tab_mime), "application/net.kovidgoyal.kitty-tab-%d", getpid());
|
|
snprintf(window_mime, sizeof(window_mime), "application/net.kovidgoyal.kitty-window-%d", getpid());
|
|
NSMutableArray *types = [NSMutableArray arrayWithObjects:
|
|
NSPasteboardTypeFileURL, NSPasteboardTypeString, NSPasteboardTypeURL, NSPasteboardTypeColor,
|
|
NSPasteboardTypeFont, NSPasteboardTypeHTML, NSPasteboardTypePDF, NSPasteboardTypePNG,
|
|
NSPasteboardTypeRTF, NSPasteboardTypeSound, NSPasteboardTypeTIFF,
|
|
UTTypeData.identifier, UTTypeItem.identifier, UTTypeContent.identifier,
|
|
mime_to_uti(tab_mime), mime_to_uti(window_mime),
|
|
nil];
|
|
// Add file promise types
|
|
[types addObjectsFromArray:[NSFilePromiseReceiver readableDraggedTypes]];
|
|
[self registerForDraggedTypes:types];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[trackingArea release];
|
|
[markedText release];
|
|
[dragging_source release];
|
|
if (input_source_at_last_key_event) [input_source_at_last_key_event release];
|
|
[input_context release];
|
|
[super dealloc];
|
|
}
|
|
|
|
- (void) removeGLFWWindow { window = NULL; }
|
|
|
|
- (GLFWDraggingSource*)draggingSource { return dragging_source; }
|
|
|
|
- (_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;
|
|
window->ns.live_resize_in_progress = true;
|
|
_glfwInputLiveResize(window, true);
|
|
}
|
|
|
|
- (void)viewDidEndLiveResize
|
|
{
|
|
if (!window) return;
|
|
window->ns.live_resize_in_progress = false;
|
|
_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);
|
|
[[NSCursor arrowCursor] set];
|
|
}
|
|
|
|
- (void)mouseEntered:(NSEvent *)event
|
|
{
|
|
(void)event;
|
|
if (!window) return;
|
|
_glfwInputCursorEnter(window, true);
|
|
updateCursorImage(window);
|
|
}
|
|
|
|
- (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 */ || (keycode == 0x04 /* h */ && mods == GLFW_MOD_CONTROL)) [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;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
is_modifier_pressed(NSUInteger flags, NSUInteger target_mask, NSUInteger other_mask, NSUInteger either_mask) {
|
|
bool target_pressed = (flags & target_mask) != 0;
|
|
bool other_pressed = (flags & other_mask) != 0;
|
|
bool either_pressed = (flags & either_mask) != 0;
|
|
if (either_pressed != (target_pressed || other_pressed)) return either_pressed;
|
|
return target_pressed;
|
|
}
|
|
|
|
- (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";
|
|
|
|
// Code for handling modifier key events copied form SDL_cocoakeyboard.m, with thanks. See IsModifierKeyPressedFunction()
|
|
#define action_for(modname, target_mask, other_mask, either_mask) action = is_modifier_pressed([event modifierFlags], target_mask, other_mask, either_mask) ? GLFW_PRESS : GLFW_RELEASE; mod_name = #modname; break;
|
|
switch(key) {
|
|
case GLFW_FKEY_CAPS_LOCK:
|
|
mod_name = "capslock";
|
|
action = modifierFlags & NSEventModifierFlagCapsLock ? GLFW_PRESS : GLFW_RELEASE; break;
|
|
case GLFW_FKEY_LEFT_SUPER: action_for(super, NX_DEVICELCMDKEYMASK, NX_DEVICERCMDKEYMASK, NX_COMMANDMASK);
|
|
case GLFW_FKEY_RIGHT_SUPER: action_for(super, NX_DEVICERCMDKEYMASK, NX_DEVICELCMDKEYMASK, NX_COMMANDMASK);
|
|
case GLFW_FKEY_LEFT_CONTROL: action_for(ctrl, NX_DEVICELCTLKEYMASK, NX_DEVICERCTLKEYMASK, NX_CONTROLMASK);
|
|
case GLFW_FKEY_RIGHT_CONTROL: action_for(ctrl, NX_DEVICERCTLKEYMASK, NX_DEVICELCTLKEYMASK, NX_CONTROLMASK);
|
|
case GLFW_FKEY_LEFT_ALT: action_for(alt, NX_DEVICELALTKEYMASK, NX_DEVICERALTKEYMASK, NX_ALTERNATEMASK);
|
|
case GLFW_FKEY_RIGHT_ALT: action_for(alt, NX_DEVICERALTKEYMASK, NX_DEVICELALTKEYMASK, NX_ALTERNATEMASK);
|
|
case GLFW_FKEY_LEFT_SHIFT: action_for(shift, NX_DEVICELSHIFTKEYMASK, NX_DEVICERSHIFTKEYMASK, NX_SHIFTMASK);
|
|
case GLFW_FKEY_RIGHT_SHIFT: action_for(shift, NX_DEVICERSHIFTKEYMASK, NX_DEVICELSHIFTKEYMASK, NX_SHIFTMASK);
|
|
default:
|
|
return;
|
|
}
|
|
#undef action_for
|
|
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
|
|
{
|
|
GLFWScrollEvent ev = {
|
|
.keyboard_modifiers=translateFlags([event modifierFlags]), .unscaled.x = [event scrollingDeltaX], .unscaled.y = [event scrollingDeltaY]};
|
|
ev.x_offset = ev.unscaled.x;
|
|
ev.y_offset = ev.unscaled.y;
|
|
if ([event hasPreciseScrollingDeltas]) {
|
|
ev.offset_type = GLFW_SCROLL_OFFEST_HIGHRES;
|
|
float xscale = 1, yscale = 1;
|
|
_glfwPlatformGetWindowContentScale(window, &xscale, &yscale);
|
|
if (xscale > 0) ev.x_offset *= xscale;
|
|
if (yscale > 0) ev.y_offset *= yscale;
|
|
}
|
|
|
|
switch([event momentumPhase]) {
|
|
case NSEventPhaseBegan: ev.momentum_type = GLFW_MOMENTUM_PHASE_BEGAN; break;
|
|
case NSEventPhaseStationary: ev.momentum_type = GLFW_MOMENTUM_PHASE_STATIONARY; break;
|
|
case NSEventPhaseChanged: ev.momentum_type = GLFW_MOMENTUM_PHASE_ACTIVE; break;
|
|
case NSEventPhaseEnded: ev.momentum_type = GLFW_MOMENTUM_PHASE_ENDED; break;
|
|
case NSEventPhaseCancelled: ev.momentum_type = GLFW_MOMENTUM_PHASE_CANCELED; break;
|
|
case NSEventPhaseMayBegin: ev.momentum_type = GLFW_MOMENTUM_PHASE_MAY_BEGIN; break;
|
|
case NSEventPhaseNone: break;
|
|
}
|
|
|
|
_glfwInputScroll(window, &ev);
|
|
}
|
|
|
|
// Drop implementation for drag and drop {{{
|
|
// Return YES to receive periodic dragging updates even when the mouse hasn't moved.
|
|
// This allows the application to update acceptance status asynchronously.
|
|
- (BOOL)wantsPeriodicDraggingUpdates
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
static void
|
|
free_drop_data(_GLFWwindow *window) {
|
|
if (window->ns.drop_data.mimes) {
|
|
for (size_t i = 0; i < window->ns.drop_data.mimes_count; i++) free((void*)window->ns.drop_data.mimes[i]);
|
|
free(window->ns.drop_data.mimes);
|
|
}
|
|
free(window->ns.drop_data.copy_mimes); // pointer array only; strings owned by mimes[]
|
|
if (window->ns.drop_data.pasteboard) [window->ns.drop_data.pasteboard release];
|
|
if (window->ns.drop_data.data_mapping) [window->ns.drop_data.data_mapping release];
|
|
if (window->ns.drop_data.file_promise_mapping) {
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
NSError *error = nil;
|
|
for (NSString *key in window->ns.drop_data.file_promise_mapping) {
|
|
NSArray *pair = [window->ns.drop_data.file_promise_mapping objectForKey:key];
|
|
error = nil; if (pair[1] != [NSNull null]) [pair[1] closeAndReturnError:&error];
|
|
error = nil; [fileManager removeItemAtURL:pair[0] error:&error];
|
|
}
|
|
[window->ns.drop_data.file_promise_mapping release];
|
|
}
|
|
memset(&window->ns.drop_data, 0, sizeof(_GLFWDropData));
|
|
}
|
|
|
|
static void
|
|
update_drop_state(_GLFWwindow *window, size_t accepted_count, GLFWDropEventType t) {
|
|
_GLFWDropData *d = &window->ns.drop_data;
|
|
d->copy_mimes_count = accepted_count;
|
|
if (t == GLFW_DROP_ENTER || t == GLFW_DROP_MOVE) d->drag_accepted = accepted_count > 0;
|
|
}
|
|
|
|
// Reset the working copy of mimes so the next callback sees the full original
|
|
// list. Returns false on allocation failure.
|
|
static bool
|
|
reset_drop_copy_mimes(_GLFWDropData *d) {
|
|
if (d->mimes_count == 0) { d->copy_mimes_count = 0; return true; }
|
|
if (!d->copy_mimes) {
|
|
d->copy_mimes = malloc(d->mimes_count * sizeof(const char*));
|
|
if (!d->copy_mimes) return false;
|
|
}
|
|
memcpy(d->copy_mimes, d->mimes, d->mimes_count * sizeof(const char*));
|
|
d->copy_mimes_count = d->mimes_count;
|
|
return true;
|
|
}
|
|
|
|
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
|
|
{
|
|
const NSRect contentRect = [window->ns.view frame];
|
|
const NSPoint pos = [sender draggingLocation];
|
|
double xpos = pos.x;
|
|
double ypos = contentRect.size.height - pos.y;
|
|
free_drop_data(window);
|
|
|
|
// Get MIME types from the dragging pasteboard
|
|
NSPasteboard* pasteboard = [sender draggingPasteboard];
|
|
|
|
// Count total types across all pasteboard items plus 2 for uri-list and text/plain
|
|
size_t max_types = 2;
|
|
for (NSPasteboardItem* item in pasteboard.pasteboardItems) max_types += [item.types count];
|
|
NSArray *classes = @[[NSFilePromiseReceiver class]];
|
|
NSArray *receivers = [pasteboard readObjectsForClasses:classes options:@{}];
|
|
for (NSFilePromiseReceiver *receiver in receivers) max_types += [receiver.fileTypes count];
|
|
|
|
// Pre-allocate C array for MIME types
|
|
const char** mime_array = (const char**)calloc(max_types, sizeof(const char*));
|
|
if (!mime_array) return NSDragOperationNone;
|
|
|
|
size_t mime_count = 0;
|
|
|
|
// Check for common types first (use _glfw_strdup since we need to own the strings)
|
|
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
|
|
if ([pasteboard canReadObjectForClasses:@[[NSURL class]] options:options]) {
|
|
mime_array[mime_count++] = _glfw_strdup("text/uri-list");
|
|
}
|
|
if ([pasteboard canReadObjectForClasses:@[[NSString class]] options:nil]) {
|
|
mime_array[mime_count++] = _glfw_strdup("text/plain");
|
|
}
|
|
#define add_mime(uti) { \
|
|
const char* mime = uti_to_mime(uti); \
|
|
if (mime && mime[0]) { \
|
|
bool duplicate = false; \
|
|
for (size_t i = 0; i < mime_count; i++) { \
|
|
if (strcmp(mime_array[i], mime) == 0) { \
|
|
duplicate = true; \
|
|
break; \
|
|
} \
|
|
} \
|
|
if (!duplicate) mime_array[mime_count++] = _glfw_strdup(mime); \
|
|
} \
|
|
}
|
|
// Get file promise based types
|
|
for (NSFilePromiseReceiver *receiver in receivers) {
|
|
for (NSString *uti in receiver.fileTypes) {
|
|
add_mime(uti);
|
|
}
|
|
}
|
|
|
|
// Get additional types from pasteboard items
|
|
for (NSPasteboardItem* item in pasteboard.pasteboardItems) {
|
|
for (NSPasteboardType uti in item.types) {
|
|
add_mime(uti);
|
|
}
|
|
}
|
|
|
|
window->ns.drop_data.mimes = mime_array;
|
|
window->ns.drop_data.mimes_count = mime_count;
|
|
bool from_self = ([sender draggingSource] != nil);
|
|
_GLFWDropData *d = &window->ns.drop_data;
|
|
if (reset_drop_copy_mimes(d)) {
|
|
size_t accepted_count = _glfwInputDropEvent(window, GLFW_DROP_ENTER, xpos, ypos, d->copy_mimes, d->copy_mimes_count, from_self);
|
|
update_drop_state(window, accepted_count, GLFW_DROP_ENTER);
|
|
}
|
|
return window->ns.drop_data.drag_accepted ? NSDragOperationGeneric : NSDragOperationNone;
|
|
}
|
|
|
|
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
|
|
{
|
|
if (!window->ns.drop_data.mimes_count) return NSDragOperationNone;
|
|
const NSRect contentRect = [window->ns.view frame];
|
|
const NSPoint pos = [sender draggingLocation];
|
|
double xpos = pos.x;
|
|
double ypos = contentRect.size.height - pos.y;
|
|
|
|
bool from_self = ([sender draggingSource] != nil);
|
|
_GLFWDropData *d = &window->ns.drop_data;
|
|
if (reset_drop_copy_mimes(d)) {
|
|
size_t accepted_count = _glfwInputDropEvent(window, GLFW_DROP_MOVE, xpos, ypos, d->copy_mimes, d->copy_mimes_count, from_self);
|
|
update_drop_state(window, accepted_count, GLFW_DROP_MOVE);
|
|
}
|
|
return window->ns.drop_data.drag_accepted ? NSDragOperationGeneric : NSDragOperationNone;
|
|
}
|
|
|
|
- (void)draggingExited:(id <NSDraggingInfo>)sender
|
|
{
|
|
bool from_self = ([sender draggingSource] != nil);
|
|
_GLFWDropData *d = &window->ns.drop_data;
|
|
if (reset_drop_copy_mimes(d)) {
|
|
size_t accepted_count = _glfwInputDropEvent(window, GLFW_DROP_LEAVE, 0, 0, d->copy_mimes, d->copy_mimes_count, from_self);
|
|
update_drop_state(window, accepted_count, GLFW_DROP_LEAVE);
|
|
}
|
|
free_drop_data(window);
|
|
}
|
|
|
|
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
|
|
{
|
|
if (!window->ns.drop_data.drag_accepted) return NO;
|
|
const NSRect contentRect = [window->ns.view frame];
|
|
const NSPoint pos = [sender draggingLocation];
|
|
double xpos = pos.x;
|
|
double ypos = contentRect.size.height - pos.y;
|
|
bool from_self = ([sender draggingSource] != nil);
|
|
_GLFWDropData *d = &window->ns.drop_data;
|
|
if (!reset_drop_copy_mimes(d)) return NO;
|
|
size_t num_accepted = _glfwInputDropEvent(window, GLFW_DROP_DROP, xpos, ypos, d->copy_mimes, d->copy_mimes_count, from_self);
|
|
if (d->copy_mimes) {
|
|
update_drop_state(window, num_accepted, GLFW_DROP_DROP);
|
|
window->ns.drop_data.pasteboard = [[sender draggingPasteboard] retain];
|
|
for (size_t i = 0; i < num_accepted; i++)
|
|
_glfwPlatformRequestDropData(window, d->copy_mimes[i]);
|
|
}
|
|
// Restore first-responder status after native DnD; the drag operation can
|
|
// displace the content view from first responder, silently breaking keyboard
|
|
// input even though osw->is_focused remains true.
|
|
[window->ns.object makeFirstResponder:window->ns.view];
|
|
return YES;
|
|
}
|
|
|
|
void
|
|
_glfwPlatformRequestDropUpdate(_GLFWwindow* window UNUSED) {
|
|
// No-op since macOS is calling the drop move callback periodically anyway
|
|
// thanks to wantsPeriodicDraggingUpdates and we have no way to inform
|
|
// macOS of any changes except in the cocoa callbacks.
|
|
}
|
|
|
|
static void
|
|
send_data_available_event_on_next_event_loop_tick(GLFWid wid, const char *mime) {
|
|
char *mt = _glfw_strdup(mime);
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
_GLFWwindow *window = _glfwWindowForId(wid);
|
|
if (window) {
|
|
const char *mimes[1] = {mt};
|
|
_glfwInputDropEvent(window, GLFW_DROP_DATA_AVAILABLE, 0, 0, mimes, 1, false);
|
|
}
|
|
free(mt);
|
|
});
|
|
}
|
|
|
|
int
|
|
_glfwPlatformRequestDropData(_GLFWwindow *window UNUSED, const char *mime) {
|
|
NSPasteboard* pasteboard = window->ns.drop_data.pasteboard;
|
|
if (!pasteboard) return EINVAL;
|
|
GLFWid wid = window->id;
|
|
if (window->ns.drop_data.data_mapping == nil) window->ns.drop_data.data_mapping = [[NSMutableDictionary alloc] init];
|
|
NSArray *pair;
|
|
if ((pair = window->ns.drop_data.data_mapping[@(mime)])) {
|
|
window->ns.drop_data.data_mapping[@(mime)] = @[pair[0], @0];
|
|
send_data_available_event_on_next_event_loop_tick(wid, mime);
|
|
return 0;
|
|
}
|
|
if (window->ns.drop_data.file_promise_mapping == nil) window->ns.drop_data.file_promise_mapping = [[NSMutableDictionary alloc] init];
|
|
if ((pair = window->ns.drop_data.file_promise_mapping[@(mime)])) {
|
|
if (pair[0] == [NSNull null]) return 0; // waiting for promise
|
|
if (pair[1] != [NSNull null]) {
|
|
NSFileHandle *h = pair[1]; NSError *error = nil;
|
|
[h seekToOffset:0 error:&error];
|
|
}
|
|
send_data_available_event_on_next_event_loop_tick(wid, mime);
|
|
return 0;
|
|
}
|
|
NSData* data = nil; NSFilePromiseReceiver *file_promise = nil;
|
|
// Handle special MIME types
|
|
if (strcmp(mime, "text/uri-list") == 0) {
|
|
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
|
|
NSArray* urls = [pasteboard readObjectsForClasses:@[[NSURL class]] options:options];
|
|
if (urls && [urls count] > 0) {
|
|
NSMutableString *uri_list = [NSMutableString stringWithCapacity:4096];
|
|
for (NSURL* url in urls) {
|
|
if ([uri_list length] > 0) [uri_list appendString:@"\n"];
|
|
if (url.fileURL) [uri_list appendString:url.filePathURL.absoluteString];
|
|
else [uri_list appendString:url.absoluteString];
|
|
}
|
|
data = [uri_list dataUsingEncoding:NSUTF8StringEncoding];
|
|
}
|
|
} else if (strcmp(mime, "text/plain") == 0 || strcmp(mime, "text/plain;charset=utf-8") == 0) {
|
|
NSArray* strings = [pasteboard readObjectsForClasses:@[[NSString class]] options:nil];
|
|
if (strings && [strings count] > 0) {
|
|
NSString* str = strings[0];
|
|
data = [str dataUsingEncoding:NSUTF8StringEncoding];
|
|
}
|
|
}
|
|
if (data == nil) {
|
|
// Try to read data for other MIME types using UTI
|
|
NSString* uti = mime_to_uti(mime);
|
|
if (uti) {
|
|
NSPasteboardType pbType = [pasteboard availableTypeFromArray:@[uti]];
|
|
if (pbType) data = [pasteboard dataForType:pbType];
|
|
}
|
|
if (data == nil) {
|
|
// look in the file promise providers
|
|
NSArray *receivers = [pasteboard readObjectsForClasses:@[[NSFilePromiseReceiver class]] options:@{}];
|
|
for (NSFilePromiseReceiver *receiver in receivers) {
|
|
for (NSString *uti in receiver.fileTypes) {
|
|
const char *q = uti_to_mime(uti);
|
|
if (q && strcmp(q, mime) == 0) {
|
|
file_promise = receiver;
|
|
break;
|
|
}
|
|
}
|
|
if (file_promise) break;
|
|
}
|
|
}
|
|
}
|
|
if (!data && !file_promise) return ENOENT;
|
|
if (file_promise != nil) {
|
|
window->ns.drop_data.file_promise_mapping[@(mime)] = @[[NSNull null], [NSNull null], [NSNull null]];
|
|
char *mt = _glfw_strdup(mime);
|
|
[file_promise receivePromisedFilesAtDestination:[NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES]
|
|
options:@{} operationQueue:[NSOperationQueue mainQueue] reader:^(NSURL *fileURL, NSError *errorOrNil) {
|
|
_GLFWwindow *window = _glfwWindowForId(wid);
|
|
if (!window || !window->ns.drop_data.file_promise_mapping) return;
|
|
id null = [NSNull null];
|
|
if (errorOrNil) {
|
|
NSLog(@"Error receiving file: %@: %@", fileURL, errorOrNil);
|
|
window->ns.drop_data.file_promise_mapping[@(mt)] = @[fileURL, null, errorOrNil];
|
|
} else {
|
|
NSError *err = nil;
|
|
NSFileHandle *file_handle = [NSFileHandle fileHandleForReadingFromURL:fileURL error:&err];
|
|
window->ns.drop_data.file_promise_mapping[@(mt)] = err ? @[fileURL, null, err] : @[fileURL, file_handle, null];
|
|
}
|
|
const char *mimes[1] = {mt};
|
|
_glfwInputDropEvent(window, GLFW_DROP_DATA_AVAILABLE, 0, 0, mimes, 1, false);
|
|
free(mt);
|
|
}];
|
|
} else {
|
|
window->ns.drop_data.data_mapping[@(mime)] = @[data, @0];
|
|
const char *mimes[1] = {mime};
|
|
_glfwInputDropEvent(window, GLFW_DROP_DATA_AVAILABLE, 0, 0, mimes, 1, false);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
ssize_t
|
|
_glfwPlatformReadAvailableDropData(GLFWwindow *w, GLFWDropEvent *ev, char *buffer, size_t capacity) {
|
|
_GLFWwindow *window = (_GLFWwindow*)w; const char *mime = ev->mimes[0];
|
|
NSArray *pair;
|
|
if ((pair = window->ns.drop_data.data_mapping[@(mime)])) {
|
|
NSData *data = pair[0];
|
|
size_t offset = [pair[1] unsignedIntegerValue];
|
|
NSUInteger dataLength = [data length];
|
|
if (offset >= dataLength) return 0; // EOF
|
|
NSUInteger remaining = dataLength - offset;
|
|
NSUInteger to_read = (remaining < capacity) ? remaining : capacity;
|
|
[data getBytes:buffer range:NSMakeRange(offset, to_read)];
|
|
offset += to_read;
|
|
window->ns.drop_data.data_mapping[@(mime)] = @[data, @(offset)];
|
|
if (to_read) send_data_available_event_on_next_event_loop_tick(window->id, mime);
|
|
return (ssize_t)to_read;
|
|
}
|
|
if ((pair = window->ns.drop_data.file_promise_mapping[@(mime)])) {
|
|
id null = [NSNull null];
|
|
if (pair[0] == null) { return -ENOENT; }
|
|
if (pair[2] != null) {
|
|
NSError *err = pair[2];
|
|
if ([err.domain isEqualToString:NSPOSIXErrorDomain]) return -err.code;
|
|
NSError *underlyingError = err.userInfo[NSUnderlyingErrorKey];
|
|
if (underlyingError && [underlyingError.domain isEqualToString:NSPOSIXErrorDomain]) return -underlyingError.code;
|
|
return -EIO;
|
|
}
|
|
NSFileHandle *h = pair[1];
|
|
int fd = h.fileDescriptor;
|
|
ssize_t bytesRead; do {
|
|
bytesRead = read(fd, buffer, capacity);
|
|
} while (bytesRead == -1 && errno == EINTR);
|
|
bytesRead = bytesRead < 0 ? -errno : bytesRead;
|
|
if (bytesRead > 0) send_data_available_event_on_next_event_loop_tick(window->id, mime);
|
|
return bytesRead;
|
|
}
|
|
return -ENOENT;
|
|
}
|
|
|
|
void
|
|
_glfwPlatformEndDrop(GLFWwindow *w UNUSED, GLFWDragOperationType op UNUSED) {
|
|
free_drop_data((_GLFWwindow*)w);
|
|
}
|
|
// }}}
|
|
|
|
- (BOOL)hasMarkedText
|
|
{
|
|
return [markedText length] > 0;
|
|
}
|
|
|
|
- (NSRange)markedRange
|
|
{
|
|
if ([markedText length] > 0)
|
|
return NSMakeRange(0, [markedText length] - 1);
|
|
else
|
|
return kEmptyRange;
|
|
}
|
|
|
|
- (NSRange)selectedRange
|
|
{
|
|
// Return position 0 with no selection to indicate text can be inserted.
|
|
// This is required for macOS dictation to work - returning kEmptyRange
|
|
// (NSNotFound, 0) causes dictation to fail because the system doesn't
|
|
// know where to insert text. See https://github.com/kovidgoyal/kitty/issues/3732
|
|
return NSMakeRange(0, 0);
|
|
}
|
|
|
|
- (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 (window && _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
|
|
size_t existing_length = strnlen(_glfw.ns.text, sizeof(_glfw.ns.text));
|
|
size_t required_length = strlen(utf8) + 1;
|
|
size_t available_length = sizeof(_glfw.ns.text) - existing_length;
|
|
if (available_length >= required_length) {
|
|
memcpy(_glfw.ns.text + existing_length, utf8, required_length); // copies the null terminator from utf8 as well
|
|
_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]);
|
|
if (forward_dictation_selector_to_app(selector, self)) return;
|
|
}
|
|
|
|
- (void)startDictation:(id)sender
|
|
{
|
|
forward_dictation_selector_to_app(_cmd, sender);
|
|
}
|
|
|
|
- (void)stopDictation:(id)sender
|
|
{
|
|
forward_dictation_selector_to_app(_cmd, sender);
|
|
}
|
|
|
|
- (BOOL)isAccessibilityElement
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector
|
|
{
|
|
// Allow accessibility selectors needed for dictation and other accessibility features
|
|
// See https://github.com/kovidgoyal/kitty/issues/3732
|
|
if (selector == @selector(accessibilityRole) ||
|
|
selector == @selector(accessibilitySelectedText) ||
|
|
selector == @selector(accessibilitySelectedTextRange) ||
|
|
selector == @selector(accessibilityNumberOfCharacters) ||
|
|
selector == @selector(accessibilityInsertionPointLineNumber) ||
|
|
selector == @selector(accessibilityValue) ||
|
|
selector == @selector(setAccessibilityValue:)) return YES;
|
|
|
|
// Allow accessibility selectors needed for external window management tools
|
|
// (e.g. Easy Move+Resize) to find and manipulate the window.
|
|
// See https://github.com/kovidgoyal/kitty/issues/5561
|
|
if (selector == @selector(accessibilityWindow) ||
|
|
selector == @selector(accessibilityParent) ||
|
|
selector == @selector(accessibilityPosition) ||
|
|
selector == @selector(setAccessibilityPosition:) ||
|
|
selector == @selector(accessibilitySize) ||
|
|
selector == @selector(setAccessibilitySize:)) 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;
|
|
}
|
|
|
|
// Accessibility methods required for dictation support
|
|
// See https://github.com/kovidgoyal/kitty/issues/3732
|
|
|
|
- (NSRange)accessibilitySelectedTextRange
|
|
{
|
|
// Return position 0 with no selection for dictation support
|
|
return NSMakeRange(0, 0);
|
|
}
|
|
|
|
- (NSInteger)accessibilityNumberOfCharacters
|
|
{
|
|
// Terminal doesn't have a fixed text buffer, return 0
|
|
return 0;
|
|
}
|
|
|
|
- (NSInteger)accessibilityInsertionPointLineNumber
|
|
{
|
|
// Return line 0 as the insertion point
|
|
return 0;
|
|
}
|
|
|
|
- (NSString *)accessibilityValue
|
|
{
|
|
// Terminal doesn't expose its buffer as an accessibility value
|
|
return @"";
|
|
}
|
|
|
|
- (void)setAccessibilityValue:(NSString *)value
|
|
{
|
|
// When dictation or other accessibility features set text, insert it as keyboard input
|
|
if (value && [value length] > 0 && window) {
|
|
const char *utf8 = [value UTF8String];
|
|
debug_key("Inserting text via setAccessibilityValue: %s\n", utf8);
|
|
GLFWkeyevent glfw_keyevent = {.text=utf8, .ime_state=GLFW_IME_COMMIT_TEXT};
|
|
_glfwInputKeyboard(window, &glfw_keyevent);
|
|
}
|
|
}
|
|
|
|
// <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
|
|
// For example, after selecting an absolute path, open the global menu bar kitty->Services and click `Show in Finder`.
|
|
- (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
|
|
// For example, open System Settings->Keyboard->Keyboard Shortcuts->Services->Text, enable `Convert Text to Full Width`, select some text and execute the service.
|
|
- (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) {
|
|
// The service wants us to replace the selection, but we can't replace anything but insert text.
|
|
const char *utf8 = polymorphic_string_as_utf8(text);
|
|
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);
|
|
// Restore pre-edit text after inserting the received text
|
|
if ([self hasMarkedText]) {
|
|
glfw_keyevent.text = [[markedText string] UTF8String];
|
|
glfw_keyevent.ime_state = GLFW_IME_PREEDIT_CHANGED;
|
|
_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;
|
|
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
|
[center addObserver:self selector:@selector(screenParametersDidChange:) name:NSApplicationDidChangeScreenParametersNotification object:nil];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)screenParametersDidChange:(NSNotification *)notification {
|
|
if (!glfw_window || !glfw_window->ns.layer_shell.is_active) return;
|
|
_glfwPlatformSetLayerShellConfig(glfw_window, NULL);
|
|
}
|
|
|
|
- (void) removeGLFWWindow
|
|
{
|
|
glfw_window = NULL;
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
- (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];
|
|
}
|
|
|
|
- (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(nullable NSScreen *)screen
|
|
{
|
|
if (glfw_window && glfw_window->ns.suppress_frame_constraints) return frameRect;
|
|
return [super constrainFrameRect:frameRect toScreen:screen];
|
|
}
|
|
|
|
- (BOOL)canBecomeKeyWindow
|
|
{
|
|
if (!glfw_window) return NO;
|
|
if (glfw_window->ns.layer_shell.is_active) {
|
|
if (glfw_window->ns.layer_shell.config.type == GLFW_LAYER_SHELL_BACKGROUND) return NO;
|
|
switch(glfw_window->ns.layer_shell.config.focus_policy) {
|
|
case GLFW_FOCUS_NOT_ALLOWED: return NO;
|
|
case GLFW_FOCUS_EXCLUSIVE: return YES;
|
|
case GLFW_FOCUS_ON_DEMAND: return YES;
|
|
}
|
|
}
|
|
// Required for NSWindowStyleMaskBorderless windows
|
|
// Also miniaturized windows should not become key
|
|
return !_glfwPlatformWindowIconified(glfw_window);
|
|
}
|
|
|
|
- (BOOL)canBecomeMainWindow
|
|
{
|
|
return !glfw_window->ns.layer_shell.is_active || glfw_window->ns.layer_shell.config.type != GLFW_LAYER_SHELL_BACKGROUND;
|
|
}
|
|
|
|
static void apply_titlebar_color_settings(_GLFWwindow *window);
|
|
|
|
static void
|
|
update_titlebar_button_visibility_after_fullscreen_transition(_GLFWwindow* w, bool traditional, bool made_fullscreen) {
|
|
// Update window button visibility
|
|
if (w->ns.titlebar_hidden) {
|
|
NSWindow *window = w->ns.object;
|
|
// 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];
|
|
}
|
|
if (!made_fullscreen) apply_titlebar_color_settings(w);
|
|
}
|
|
|
|
- (void)toggleFullScreen:(nullable id)sender
|
|
{
|
|
if (glfw_window) {
|
|
if (glfw_window->ns.in_fullscreen_transition) return;
|
|
// Capture the windowed frame before any fullscreen transition begins.
|
|
// This is more reliable than saving it inside _glfwPlatformToggleFullscreen
|
|
// because setStyleMask: calls between cycles can reposition the window (#9572).
|
|
if (!glfw_window->ns.in_traditional_fullscreen && !([self styleMask] & NSWindowStyleMaskFullScreen)) {
|
|
glfw_window->ns.pre_traditional_fullscreen_frame = [self frame];
|
|
}
|
|
if (glfw_window->ns.toggleFullscreenCallback && glfw_window->ns.toggleFullscreenCallback((GLFWwindow*)glfw_window) == 1) return;
|
|
glfw_window->ns.in_fullscreen_transition = true;
|
|
}
|
|
NSWindowStyleMask sm = [self styleMask];
|
|
bool is_fullscreen_already = (sm & NSWindowStyleMaskFullScreen) != 0;
|
|
// 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];
|
|
update_titlebar_button_visibility_after_fullscreen_transition(glfw_window, false, !is_fullscreen_already);
|
|
}
|
|
|
|
- (void)zoom:(id)sender
|
|
{
|
|
if (![self isZoomed]) {
|
|
const NSSize original = [self resizeIncrements];
|
|
[self setResizeIncrements:NSMakeSize(1.0, 1.0)];
|
|
[super zoom:sender];
|
|
[self setResizeIncrements:original];
|
|
} else {
|
|
[super zoom:sender];
|
|
}
|
|
}
|
|
|
|
@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];
|
|
CGRect screen_frame = [[(NSWindow*) window->ns.object screen] frame];
|
|
if (CGRectContainsPoint(screen_frame, _glfw.ns.cascadePoint)
|
|
|| CGPointEqualToPoint(CGPointZero, _glfw.ns.cascadePoint)) {
|
|
_glfw.ns.cascadePoint =
|
|
NSPointToCGPoint([window->ns.object cascadeTopLeftFromPoint:
|
|
NSPointFromCGPoint(_glfw.ns.cascadePoint)]);
|
|
} else {
|
|
_glfw.ns.cascadePoint = CGPointZero;
|
|
}
|
|
|
|
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);
|
|
if (wndconfig->blur_radius > 0) _glfwPlatformSetWindowBlur(window, wndconfig->blur_radius);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
////// GLFW platform API //////
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
int _glfwPlatformCreateWindow(_GLFWwindow* window, const _GLFWwndconfig* wndconfig, const _GLFWctxconfig* ctxconfig, const _GLFWfbconfig* fbconfig, const GLFWLayerShellConfig *lsc) {
|
|
window->ns.deadKeyState = 0;
|
|
if (lsc) {
|
|
window->ns.layer_shell.is_active = true;
|
|
window->ns.layer_shell.config = *lsc;
|
|
} else window->ns.layer_shell.is_active = false;
|
|
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)
|
|
{
|
|
GLFWWindow *w = window->ns.object;
|
|
if (_glfw.ns.disabledCursorWindow == window)
|
|
_glfw.ns.disabledCursorWindow = NULL;
|
|
free_drop_data(window);
|
|
if (window->ns.notch_cover_window) {
|
|
[w removeChildWindow:window->ns.notch_cover_window];
|
|
[window->ns.notch_cover_window close];
|
|
[window->ns.notch_cover_window release];
|
|
window->ns.notch_cover_window = nil;
|
|
}
|
|
|
|
[w orderOut:nil];
|
|
|
|
if (window->monitor)
|
|
releaseMonitor(window);
|
|
|
|
if (window->context.destroy)
|
|
window->context.destroy(window);
|
|
|
|
[w setDelegate:nil];
|
|
[window->ns.delegate cleanup];
|
|
[window->ns.delegate release];
|
|
window->ns.delegate = nil;
|
|
|
|
[window->ns.view removeGLFWWindow];
|
|
[window->ns.view release];
|
|
window->ns.view = nil;
|
|
|
|
[w removeGLFWWindow];
|
|
// Workaround for macOS Tahoe where if the frame is not set to zero size
|
|
// even after NSWindow::close the window remains on screen as an invisible
|
|
// rectangle that intercepts mouse events and takes up space in mission
|
|
// control. Sigh.
|
|
NSRect frame = w.frame; frame.size.width = 0; frame.size.height = 0;
|
|
[w setFrame:frame display:NO];
|
|
[w setHasShadow:NO];
|
|
[w close]; // sends a release to NSWindow so we dont release it
|
|
window->ns.object = nil;
|
|
}
|
|
|
|
static NSScreen*
|
|
screen_for_window_center(_GLFWwindow *window) {
|
|
NSRect windowFrame = [window->ns.object frame];
|
|
NSPoint windowCenter = NSMakePoint(NSMidX(windowFrame), NSMidY(windowFrame));
|
|
for (NSScreen *screen in [NSScreen screens]) {
|
|
if (NSPointInRect(windowCenter, [screen frame])) {
|
|
return screen;
|
|
}
|
|
}
|
|
return NSScreen.mainScreen;
|
|
}
|
|
|
|
static NSScreen*
|
|
active_screen(void) {
|
|
NSPoint mouseLocation = [NSEvent mouseLocation];
|
|
NSArray<NSScreen *> *screens = [NSScreen screens];
|
|
for (NSScreen *screen in screens) {
|
|
if (NSPointInRect(mouseLocation, [screen frame])) {
|
|
return screen;
|
|
}
|
|
}
|
|
// As a fallback, return the main screen
|
|
return [NSScreen mainScreen];
|
|
}
|
|
|
|
static bool
|
|
is_same_screen(NSScreen *screenA, NSScreen * screenB) {
|
|
if (screenA == screenB) return true;
|
|
NSDictionary<NSDeviceDescriptionKey, id> *deviceDescriptionA = [screenA deviceDescription];
|
|
NSDictionary<NSDeviceDescriptionKey, id> *deviceDescriptionB = [screenB deviceDescription];
|
|
NSNumber *screenNumberA = deviceDescriptionA[@"NSScreenNumber"];
|
|
NSNumber *screenNumberB = deviceDescriptionB[@"NSScreenNumber"];
|
|
return [screenNumberA isEqualToNumber:screenNumberB];
|
|
}
|
|
|
|
static void
|
|
move_window_to_screen(_GLFWwindow *window, NSScreen *target) {
|
|
NSRect screenFrame = [target visibleFrame];
|
|
NSRect windowFrame = [window->ns.object frame];
|
|
CGFloat newX = NSMidX(screenFrame) - (windowFrame.size.width / 2.0);
|
|
CGFloat newY = NSMidY(screenFrame) - (windowFrame.size.height / 2.0);
|
|
NSRect newWindowFrame = NSMakeRect(newX, newY, windowFrame.size.width, windowFrame.size.height);
|
|
[window->ns.object setFrame:newWindowFrame display:NO animate:NO];
|
|
if (window->ns.layer_shell.is_active) _glfwPlatformSetLayerShellConfig(window, NULL);
|
|
}
|
|
|
|
const GLFWLayerShellConfig*
|
|
_glfwPlatformGetLayerShellConfig(_GLFWwindow *window) {
|
|
return &window->ns.layer_shell.config;
|
|
}
|
|
|
|
static NSScreen*
|
|
screen_for_name(const char *name) {
|
|
int count = 0;
|
|
GLFWmonitor **monitors = glfwGetMonitors(&count);
|
|
for (int i = 0; i < count; i++) {
|
|
const char *q = glfwGetMonitorName(monitors[i]);
|
|
if (q && strcmp(q, name) == 0) return ((_GLFWmonitor*)monitors[i])->ns.screen;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
bool
|
|
_glfwPlatformSetLayerShellConfig(_GLFWwindow* window, const GLFWLayerShellConfig *value) {
|
|
#define config window->ns.layer_shell.config
|
|
#define nswindow window->ns.object
|
|
window->resizable = false;
|
|
if (value) config = *value;
|
|
const bool is_transparent = _glfwPlatformFramebufferTransparent(window);
|
|
int background_blur = config.related.background_blur;
|
|
if (!is_transparent || config.related.background_opacity >= 1.f) { background_blur = 0; }
|
|
[nswindow setBackgroundColor:[NSColor clearColor]];
|
|
_glfwPlatformSetWindowBlur(window, background_blur);
|
|
window->ns.titlebar_hidden = true;
|
|
window->decorated = false;
|
|
[nswindow setTitlebarAppearsTransparent:false];
|
|
[nswindow setHasShadow:false];
|
|
[nswindow setTitleVisibility:NSWindowTitleHidden];
|
|
NSColorSpace *cs = nil;
|
|
switch (config.related.color_space) {
|
|
case SRGB_COLORSPACE: cs = [NSColorSpace sRGBColorSpace]; break;
|
|
case DISPLAY_P3_COLORSPACE: cs = [NSColorSpace displayP3ColorSpace]; break;
|
|
case DEFAULT_COLORSPACE: cs = nil; break; // using deviceRGBColorSpace causes a hang when transitioning to fullscreen
|
|
}
|
|
[nswindow setColorSpace:cs];
|
|
[[nswindow standardWindowButton: NSWindowCloseButton] setHidden:true];
|
|
[[nswindow standardWindowButton: NSWindowMiniaturizeButton] setHidden:true];
|
|
[[nswindow standardWindowButton: NSWindowZoomButton] setHidden:true];
|
|
[nswindow setStyleMask:NSWindowStyleMaskBorderless];
|
|
// HACK: Changing the style mask can cause the first responder to be cleared
|
|
[nswindow makeFirstResponder:window->ns.view];
|
|
NSScreen *screen = screen_for_window_center(window);
|
|
if (config.output_name[0]) {
|
|
NSScreen *q = screen_for_name(config.output_name);
|
|
if (q) screen = q;
|
|
}
|
|
unsigned cell_width, cell_height; double left_edge_spacing, top_edge_spacing, right_edge_spacing, bottom_edge_spacing;
|
|
float xscale = (float)config.expected.xscale, yscale = (float)config.expected.yscale;
|
|
_glfwPlatformGetWindowContentScale(window, &xscale, &yscale);
|
|
config.size_callback((GLFWwindow*)window, xscale, yscale, &cell_width, &cell_height, &left_edge_spacing, &top_edge_spacing, &right_edge_spacing, &bottom_edge_spacing);
|
|
double spacing_x = left_edge_spacing + right_edge_spacing;
|
|
double spacing_y = top_edge_spacing + bottom_edge_spacing;
|
|
const unsigned xsz = config.x_size_in_pixels ? (unsigned)(config.x_size_in_pixels * xscale) : (cell_width * config.x_size_in_cells);
|
|
const unsigned ysz = config.y_size_in_pixels ? (unsigned)(config.y_size_in_pixels * yscale) : (cell_height * config.y_size_in_cells);
|
|
CGFloat dock_height = NSMinY(screen.visibleFrame) - NSMinY(screen.frame);
|
|
CGFloat menubar_height = NSHeight(screen.frame) - NSHeight(screen.visibleFrame) - dock_height;
|
|
CGFloat x = NSMinX(screen.visibleFrame), y = NSMinY(screen.visibleFrame) - 1, width = NSWidth(screen.visibleFrame), height = NSHeight(screen.visibleFrame) + 2;
|
|
if (config.type == GLFW_LAYER_SHELL_BACKGROUND || config.edge == GLFW_EDGE_CENTER) {
|
|
x = NSMinX(screen.frame); height = NSHeight(screen.frame) - menubar_height + 1; y = NSMinY(screen.frame); width = NSWidth(screen.frame);
|
|
}
|
|
// Screen co-ordinate system is with origin in lower left and y increasing upwards and x increasing rightwards
|
|
// NSLog(@"frame: %@ visibleFrame: %@\n", NSStringFromRect(screen.frame), NSStringFromRect(screen.visibleFrame));
|
|
NSWindowLevel level = NSPopUpMenuWindowLevel - 1; // so that popup menus from globalmenubar function
|
|
NSWindowAnimationBehavior animation_behavior = NSWindowAnimationBehaviorUtilityWindow;
|
|
switch (config.type) {
|
|
case GLFW_LAYER_SHELL_BACKGROUND:
|
|
animation_behavior = NSWindowAnimationBehaviorNone;
|
|
// See: https://stackoverflow.com/questions/4982584/how-do-i-draw-the-desktop-on-mac-os-x/4982619#4982619
|
|
level = kCGDesktopWindowLevel;
|
|
break;
|
|
case GLFW_LAYER_SHELL_OVERLAY: case GLFW_LAYER_SHELL_NONE: break;
|
|
case GLFW_LAYER_SHELL_PANEL: level = NSNormalWindowLevel - 1; break;
|
|
case GLFW_LAYER_SHELL_TOP: level--; break;
|
|
}
|
|
if (config.type != GLFW_LAYER_SHELL_BACKGROUND && config.edge != GLFW_EDGE_CENTER) {
|
|
double panel_height = spacing_y + ysz / yscale, panel_width = spacing_x + xsz / xscale;
|
|
switch (config.edge) {
|
|
case GLFW_EDGE_BOTTOM: height = panel_height; break;
|
|
case GLFW_EDGE_TOP:
|
|
y += height - panel_height + 1.;
|
|
height = panel_height;
|
|
break;
|
|
case GLFW_EDGE_LEFT: width = panel_width; break;
|
|
case GLFW_EDGE_RIGHT:
|
|
x += width - panel_width + 1.;
|
|
width = panel_width;
|
|
break;
|
|
case GLFW_EDGE_CENTER_SIZED:
|
|
x += (width - panel_width) / 2;
|
|
y += (height - panel_height) / 2;
|
|
width = panel_width; height = panel_height;
|
|
break;
|
|
default: // top left
|
|
y += height - panel_height + 1.;
|
|
height = panel_height; width = panel_width;
|
|
break;
|
|
}
|
|
if (width < 1.) width = NSWidth(screen.visibleFrame);
|
|
if (height < 1.) height = NSWidth(screen.visibleFrame);
|
|
}
|
|
|
|
if (config.edge != GLFW_EDGE_CENTER_SIZED) {
|
|
x += config.requested_left_margin; width -= config.requested_left_margin + config.requested_right_margin;
|
|
y += config.requested_bottom_margin; height -= config.requested_top_margin + config.requested_bottom_margin;
|
|
}
|
|
|
|
[nswindow setAnimationBehavior:animation_behavior];
|
|
[nswindow setLevel:level];
|
|
[nswindow setCollectionBehavior: (NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorIgnoresCycle)];
|
|
[nswindow setFrame:NSMakeRect(x, y, width, height) display:YES];
|
|
return true;
|
|
#undef config
|
|
#undef nswindow
|
|
}
|
|
|
|
void _glfwPlatformSetWindowTitle(_GLFWwindow* window, const char* title)
|
|
{
|
|
if (!title) return;
|
|
NSString* string = @(title);
|
|
if (!string) return; // the runtime failed to convert title to an NSString
|
|
[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->ns.layer_shell.is_active) return;
|
|
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 _glfwPlatformGetKeyboardRepeatDelay(monotonic_t *delay, monotonic_t *interval)
|
|
{
|
|
if (delay) *delay = s_double_to_monotonic_t([NSEvent keyRepeatDelay]);
|
|
if (interval) *interval = s_double_to_monotonic_t([NSEvent keyRepeatInterval]);
|
|
}
|
|
|
|
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]) {
|
|
[window->ns.object zoom:nil];
|
|
}
|
|
}
|
|
|
|
void _glfwPlatformShowWindow(_GLFWwindow* window, bool move_to_active_screen)
|
|
{
|
|
const bool is_background = window->ns.layer_shell.is_active && window->ns.layer_shell.config.type == GLFW_LAYER_SHELL_BACKGROUND;
|
|
NSWindow *nw = window->ns.object;
|
|
if (move_to_active_screen) {
|
|
NSScreen *current_screen = screen_for_window_center(window);
|
|
NSScreen *target_screen = active_screen();
|
|
if (!is_same_screen(current_screen, target_screen)) {
|
|
debug_rendering("Moving OS window %llu to active screen\n", window->id);
|
|
move_window_to_screen(window, target_screen);
|
|
}
|
|
}
|
|
if (is_background) {
|
|
[nw orderBack:nil];
|
|
} else {
|
|
// Cocoa has a bug where when showing a hidden window after
|
|
// fullscreening an application, the window does not get added
|
|
// to the current space even though it has NSWindowCollectionBehaviorCanJoinAllSpaces
|
|
// probably because it wasnt added to the temp space used for
|
|
// fullscreen. So to work around that, we change the collection
|
|
// behavior temporarily to NSWindowCollectionBehaviorMoveToActiveSpace
|
|
// and then change it back asynchronously.
|
|
// See https://github.com/kovidgoyal/kitty/issues/8740
|
|
NSWindowCollectionBehavior old = nw.collectionBehavior;
|
|
nw.collectionBehavior = (old & !NSWindowCollectionBehaviorCanJoinAllSpaces) | NSWindowCollectionBehaviorMoveToActiveSpace;
|
|
[nw orderFront:nil];
|
|
__block __typeof__(nw) weakSelf = nw;
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
weakSelf.collectionBehavior = old;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _glfwPlatformHideWindow(_GLFWwindow* window)
|
|
{
|
|
[window->ns.object orderOut:nil];
|
|
pid_t prev_app_pid = _glfw.ns.previous_front_most_application; _glfw.ns.previous_front_most_application = 0;
|
|
NSRunningApplication *app;
|
|
if (window->ns.layer_shell.is_active && prev_app_pid > 0 && (app = [NSRunningApplication runningApplicationWithProcessIdentifier:prev_app_pid])) {
|
|
unsigned num_visible = 0;
|
|
for (_GLFWwindow *w = _glfw.windowListHead; w; w = w->next) {
|
|
if (_glfwPlatformWindowVisible(w)) num_visible++;
|
|
}
|
|
if (!num_visible) {
|
|
// yieldActivationToApplication was introduced in macOS 14 (Sonoma)
|
|
SEL selector = NSSelectorFromString(@"yieldActivationToApplication:");
|
|
if ([NSApp respondsToSelector:selector]) {
|
|
[NSApp performSelector:selector withObject:app];
|
|
[app activateWithOptions:0];
|
|
} else {
|
|
#define NSApplicationActivateIgnoringOtherApps 2
|
|
[app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
|
|
#undef NSApplicationActivateIgnoringOtherApps
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void _glfwPlatformRequestWindowAttention(_GLFWwindow* window UNUSED)
|
|
{
|
|
[NSApp requestUserAttention:NSInformationalRequest];
|
|
}
|
|
|
|
int _glfwPlatformWindowBell(_GLFWwindow* window UNUSED)
|
|
{
|
|
NSBeep();
|
|
return true;
|
|
}
|
|
|
|
void _glfwPlatformFocusWindow(_GLFWwindow* window)
|
|
{
|
|
if (_glfwPlatformWindowIconified(window)) {
|
|
// miniaturized windows return false in canBecomeKeyWindow therefore
|
|
// unminiaturize first
|
|
[window->ns.object deminiaturize:nil];
|
|
}
|
|
if ([window->ns.object canBecomeKeyWindow]) {
|
|
// Make us the active application
|
|
[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)];
|
|
[window->ns.object makeFirstResponder:window->ns.view];
|
|
}
|
|
|
|
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 _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;
|
|
}
|
|
|
|
static NSCursor*
|
|
load_hidden_system_cursor(NSString *name, SEL fallback) {
|
|
// this implementation comes from SDL_cocoamouse.m
|
|
NSString *cursorPath = [@"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors" stringByAppendingPathComponent:name];
|
|
NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:[cursorPath stringByAppendingPathComponent:@"info.plist"]];
|
|
/* we can't do animation atm. :/ */
|
|
const int frames = (int)[[info valueForKey:@"frames"] integerValue];
|
|
NSCursor *cursor;
|
|
NSImage *image = [[NSImage alloc] initWithContentsOfFile:[cursorPath stringByAppendingPathComponent:@"cursor.pdf"]];
|
|
if ((image == nil) || (image.isValid == NO)) {
|
|
return [NSCursor performSelector:fallback];
|
|
}
|
|
|
|
if (frames > 1) {
|
|
const NSCompositingOperation operation = NSCompositingOperationCopy;
|
|
const NSSize cropped_size = NSMakeSize(image.size.width, (int)(image.size.height / frames));
|
|
NSImage *cropped = [[NSImage alloc] initWithSize:cropped_size];
|
|
if (cropped == nil) {
|
|
return [NSCursor performSelector:fallback];
|
|
}
|
|
|
|
[cropped lockFocus];
|
|
{
|
|
const NSRect cropped_rect = NSMakeRect(0, 0, cropped_size.width, cropped_size.height);
|
|
[image drawInRect:cropped_rect fromRect:cropped_rect operation:operation fraction:1];
|
|
}
|
|
[cropped unlockFocus];
|
|
image = cropped;
|
|
}
|
|
|
|
cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSMakePoint([[info valueForKey:@"hotx"] doubleValue], [[info valueForKey:@"hoty"] doubleValue])];
|
|
return cursor;
|
|
}
|
|
|
|
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 performSelector:@selector(val)]; break;
|
|
#define S(name, val, fallback) case name: cursor->ns.object = load_hidden_system_cursor(@#val, @selector(val)); break;
|
|
switch(shape) {
|
|
/* start glfw to cocoa (auto generated by gen-key-constants.py do not edit) */
|
|
C(GLFW_DEFAULT_CURSOR, arrowCursor);
|
|
C(GLFW_TEXT_CURSOR, IBeamCursor);
|
|
C(GLFW_POINTER_CURSOR, pointingHandCursor);
|
|
S(GLFW_HELP_CURSOR, help, arrowCursor);
|
|
S(GLFW_WAIT_CURSOR, busybutclickable, arrowCursor);
|
|
S(GLFW_PROGRESS_CURSOR, busybutclickable, arrowCursor);
|
|
C(GLFW_CROSSHAIR_CURSOR, crosshairCursor);
|
|
S(GLFW_CELL_CURSOR, cell, crosshairCursor);
|
|
C(GLFW_VERTICAL_TEXT_CURSOR, IBeamCursorForVerticalLayout);
|
|
S(GLFW_MOVE_CURSOR, move, openHandCursor);
|
|
C(GLFW_E_RESIZE_CURSOR, resizeRightCursor);
|
|
S(GLFW_NE_RESIZE_CURSOR, resizenortheast, _windowResizeNorthEastSouthWestCursor);
|
|
S(GLFW_NW_RESIZE_CURSOR, resizenorthwest, _windowResizeNorthWestSouthEastCursor);
|
|
C(GLFW_N_RESIZE_CURSOR, resizeUpCursor);
|
|
S(GLFW_SE_RESIZE_CURSOR, resizesoutheast, _windowResizeNorthWestSouthEastCursor);
|
|
S(GLFW_SW_RESIZE_CURSOR, resizesouthwest, _windowResizeNorthEastSouthWestCursor);
|
|
C(GLFW_S_RESIZE_CURSOR, resizeDownCursor);
|
|
C(GLFW_W_RESIZE_CURSOR, resizeLeftCursor);
|
|
C(GLFW_EW_RESIZE_CURSOR, resizeLeftRightCursor);
|
|
C(GLFW_NS_RESIZE_CURSOR, resizeUpDownCursor);
|
|
U(GLFW_NESW_RESIZE_CURSOR, _windowResizeNorthEastSouthWestCursor);
|
|
U(GLFW_NWSE_RESIZE_CURSOR, _windowResizeNorthWestSouthEastCursor);
|
|
S(GLFW_ZOOM_IN_CURSOR, zoomin, arrowCursor);
|
|
S(GLFW_ZOOM_OUT_CURSOR, zoomout, arrowCursor);
|
|
C(GLFW_ALIAS_CURSOR, dragLinkCursor);
|
|
C(GLFW_COPY_CURSOR, dragCopyCursor);
|
|
C(GLFW_NOT_ALLOWED_CURSOR, operationNotAllowedCursor);
|
|
C(GLFW_NO_DROP_CURSOR, operationNotAllowedCursor);
|
|
C(GLFW_GRAB_CURSOR, openHandCursor);
|
|
C(GLFW_GRABBING_CURSOR, closedHandCursor);
|
|
/* end glfw to cocoa */
|
|
case GLFW_INVALID_CURSOR:
|
|
return false;
|
|
}
|
|
#undef C
|
|
#undef U
|
|
#undef S
|
|
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;
|
|
}
|
|
|
|
static void
|
|
make_window_fullscreen_after_show(unsigned long long timer_id, void* data) {
|
|
(void)timer_id;
|
|
unsigned long long window_id = (uintptr_t)data;
|
|
for (_GLFWwindow *w = _glfw.windowListHead; w; w = w->next) {
|
|
if (w->id == window_id) {
|
|
NSWindow *window = w->ns.object;
|
|
[window toggleFullScreen: nil];
|
|
update_titlebar_button_visibility_after_fullscreen_transition(w, false, true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
_glfwUpdateNotchCover(_GLFWwindow* w) {
|
|
NSWindow *window = w->ns.object;
|
|
if (w->ns.notch_cover_window) {
|
|
[window removeChildWindow:w->ns.notch_cover_window];
|
|
[w->ns.notch_cover_window close];
|
|
[w->ns.notch_cover_window release];
|
|
w->ns.notch_cover_window = nil;
|
|
}
|
|
if (!w->ns.in_traditional_fullscreen) return;
|
|
if (@available(macOS 12.0, *)) {
|
|
CGFloat insetTop = window.screen.safeAreaInsets.top;
|
|
if (insetTop <= 0) return;
|
|
NSRect sf = [window.screen frame];
|
|
NSWindow *bg_window = [[NSWindow alloc] initWithContentRect:sf styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO];
|
|
[bg_window setBackgroundColor:[NSColor clearColor]];
|
|
[bg_window setHasShadow:NO];
|
|
[bg_window setOpaque:NO];
|
|
[bg_window setIgnoresMouseEvents:YES];
|
|
[bg_window setReleasedWhenClosed:NO];
|
|
[bg_window setColorSpace:[window colorSpace]];
|
|
// Add a colored subview only in the notch strip area
|
|
NSView *notchView = [[NSView alloc] initWithFrame:NSMakeRect(0, sf.size.height - insetTop, sf.size.width, insetTop)];
|
|
notchView.wantsLayer = YES;
|
|
unsigned int c = w->ns.notch_cover_color;
|
|
float a = w->ns.notch_cover_opacity;
|
|
notchView.layer.backgroundColor = [NSColor colorWithSRGBRed:((c >> 16) & 0xFF) / 255.0
|
|
green:((c >> 8) & 0xFF) / 255.0
|
|
blue:(c & 0xFF) / 255.0
|
|
alpha:a].CGColor;
|
|
[bg_window.contentView addSubview:notchView];
|
|
// must be above otherwise shadow of main window is rendered over bg_window
|
|
[window addChildWindow:bg_window ordered:NSWindowAbove];
|
|
w->ns.notch_cover_window = bg_window;
|
|
[notchView release];
|
|
}
|
|
}
|
|
|
|
bool _glfwPlatformToggleFullscreen(_GLFWwindow* w, unsigned int flags) {
|
|
NSWindow *window = w->ns.object;
|
|
bool made_fullscreen = true;
|
|
bool traditional = !(flags & 1);
|
|
bool ignore_safe_area_insets = flags & 2;
|
|
NSWindowStyleMask sm = [window styleMask];
|
|
if (traditional) {
|
|
if (@available(macOS 10.15.7, *)) {
|
|
// As of Big Turd NSWindowStyleMaskFullScreen is no longer usable
|
|
// Also no longer compatible after a minor release of macOS 10.15.7
|
|
if (!w->ns.in_traditional_fullscreen) {
|
|
// Apple throws NSGenericException if setStyleMask: clears
|
|
// NSWindowStyleMaskFullScreen outside a transition (see #9572).
|
|
// Split View sets this flag via the system, so fall back to
|
|
// Cocoa fullscreen toggle instead of the traditional path.
|
|
if (sm & NSWindowStyleMaskFullScreen) {
|
|
[window toggleFullScreen:nil];
|
|
return false;
|
|
}
|
|
w->ns.pre_full_screen_style_mask = sm;
|
|
w->ns.pre_traditional_fullscreen_frame = [window frame];
|
|
[window setStyleMask: NSWindowStyleMaskBorderless];
|
|
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock];
|
|
NSRect screenFrame = [window.screen frame];
|
|
if (!ignore_safe_area_insets) {
|
|
if (@available(macOS 12.0, *)) {
|
|
screenFrame.size.height -= window.screen.safeAreaInsets.top;
|
|
}
|
|
}
|
|
[window setFrame:screenFrame display:YES];
|
|
w->ns.in_traditional_fullscreen = true;
|
|
if (!ignore_safe_area_insets) _glfwUpdateNotchCover(w);
|
|
} else {
|
|
made_fullscreen = false;
|
|
if (sm & NSWindowStyleMaskFullScreen) {
|
|
// Split View added NSWindowStyleMaskFullScreen on top of our
|
|
// traditional fullscreen. We can't clear that flag directly
|
|
// (NSGenericException), so trigger a Cocoa exit and defer the
|
|
// traditional fullscreen cleanup to windowDidExitFullScreen:
|
|
// which fires after macOS finishes its async transition (#9572).
|
|
// Return true to prevent the caller from setting the window
|
|
// frame during the Cocoa exit animation.
|
|
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault];
|
|
[window toggleFullScreen:nil];
|
|
return true;
|
|
} else {
|
|
[window setStyleMask: w->ns.pre_full_screen_style_mask];
|
|
[[NSApplication sharedApplication] setPresentationOptions: NSApplicationPresentationDefault];
|
|
w->ns.in_traditional_fullscreen = false;
|
|
_glfwUpdateNotchCover(w);
|
|
}
|
|
}
|
|
} 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 doesn't 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 && !_glfwPlatformWindowVisible(w)) {
|
|
// Bug in Apple's fullscreen implementation causes fullscreen to
|
|
// not work before window is shown (at creation) if another window
|
|
// is already fullscreen. Le sigh. https://github.com/kovidgoyal/kitty/issues/7448
|
|
_glfwPlatformAddTimer(0, false, make_window_fullscreen_after_show, (void*)(uintptr_t)(w->id), NULL);
|
|
return made_fullscreen;
|
|
}
|
|
if (in_fullscreen) made_fullscreen = false;
|
|
[window toggleFullScreen: nil];
|
|
}
|
|
update_titlebar_button_visibility_after_fullscreen_transition(w, traditional, made_fullscreen);
|
|
return made_fullscreen;
|
|
}
|
|
|
|
// Clipboard {{{
|
|
|
|
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
|
|
}
|
|
|
|
int
|
|
_glfwPlatformSetWindowBlur(_GLFWwindow *window, int radius) {
|
|
int orig = window->ns.blur_radius;
|
|
if (radius > -1 && radius != window->ns.blur_radius) {
|
|
extern OSStatus CGSSetWindowBackgroundBlurRadius(void* connection, NSInteger windowNumber, int radius);
|
|
extern void* CGSDefaultConnectionForThread(void);
|
|
CGSSetWindowBackgroundBlurRadius(CGSDefaultConnectionForThread(), [window->ns.object windowNumber], radius);
|
|
window->ns.blur_radius = radius;
|
|
}
|
|
return orig;
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
////// GLFW native API //////
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
GLFWAPI id glfwGetCocoaWindow(GLFWwindow* handle)
|
|
{
|
|
_GLFWwindow* window = (_GLFWwindow*) handle;
|
|
assert(window != NULL);
|
|
|
|
_GLFW_REQUIRE_INIT_OR_RETURN(nil);
|
|
return window->ns.object;
|
|
}
|
|
|
|
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 GLFWcocoarenderframefun glfwCocoaSetWindowResizeCallback(GLFWwindow *w, GLFWcocoarenderframefun cb) {
|
|
_GLFWwindow* window = (_GLFWwindow*)w;
|
|
GLFWcocoarenderframefun current = window->ns.resizeCallback;
|
|
window->ns.resizeCallback = cb;
|
|
return current;
|
|
}
|
|
|
|
@implementation NSView (FindByIdentifier)
|
|
|
|
- (NSArray<NSView *> *)viewsWithIdentifier:(NSUserInterfaceItemIdentifier)identifier {
|
|
NSMutableArray<NSView *> *result = [NSMutableArray array];
|
|
if ([self.identifier isEqual:identifier]) {
|
|
[result addObject:self];
|
|
}
|
|
for (NSView *sub in self.subviews) {
|
|
[result addObjectsFromArray:[sub viewsWithIdentifier:identifier]];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@end
|
|
|
|
static
|
|
void clear_title_bar_background_views(NSWindow *window) {
|
|
#define tag @"kitty-for-transparent-titlebar"
|
|
NSView *contentView = window.contentView, *titlebarContainer = contentView ? contentView.superview : nil;
|
|
if (titlebarContainer) {
|
|
for (NSView *subview in [titlebarContainer viewsWithIdentifier:tag]) [subview removeFromSuperview];
|
|
}
|
|
}
|
|
|
|
static void
|
|
set_title_bar_background(NSWindow *window, NSColor *backgroundColor) {
|
|
// add an extra view that just renders the background color under the transparent titlebar
|
|
NSView *contentView = window.contentView, *titlebarContainer = contentView ? contentView.superview : nil;
|
|
if (!titlebarContainer) return;
|
|
for (NSView *subview in [titlebarContainer viewsWithIdentifier:tag]) [subview removeFromSuperview];
|
|
if (!backgroundColor) return;
|
|
|
|
NSButton *b = [window standardWindowButton:NSWindowCloseButton];
|
|
if (b) {
|
|
NSView *titlebarView = b.superview;
|
|
NSView *bgView = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, titlebarView.bounds.size.width, titlebarView.bounds.size.height)];
|
|
bgView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
bgView.wantsLayer = YES;
|
|
bgView.layer.backgroundColor = backgroundColor.CGColor;
|
|
bgView.identifier = tag;
|
|
[titlebarView addSubview:bgView positioned:NSWindowBelow relativeTo:titlebarView.subviews[0]];
|
|
[NSLayoutConstraint activateConstraints:@[
|
|
// Pin to the top of the content view.
|
|
[bgView.topAnchor constraintEqualToAnchor:titlebarView.topAnchor],
|
|
// Pin to the leading edge of the content view.
|
|
[bgView.leadingAnchor constraintEqualToAnchor:titlebarView.leadingAnchor],
|
|
// Pin to the trailing edge of the content view.
|
|
[bgView.trailingAnchor constraintEqualToAnchor:titlebarView.trailingAnchor],
|
|
// Give it a fixed height
|
|
[bgView.bottomAnchor constraintEqualToAnchor:titlebarView.bottomAnchor]
|
|
]];
|
|
[bgView release];
|
|
return;
|
|
}
|
|
NSView *bgView = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, titlebarContainer.bounds.size.width, 32)];
|
|
bgView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
bgView.wantsLayer = YES;
|
|
bgView.layer.backgroundColor = backgroundColor.CGColor;
|
|
bgView.identifier = tag;
|
|
// position the background view above the content view but below the titlebar view
|
|
[titlebarContainer addSubview:bgView positioned:NSWindowAbove relativeTo:contentView];
|
|
// for (NSView *subview in titlebarContainer.subviews) NSLog(@"sv: %@", subview.identifier);
|
|
[NSLayoutConstraint activateConstraints:@[
|
|
// Pin to the top of the content view.
|
|
[bgView.topAnchor constraintEqualToAnchor:titlebarContainer.topAnchor],
|
|
// Pin to the leading edge of the content view.
|
|
[bgView.leadingAnchor constraintEqualToAnchor:titlebarContainer.leadingAnchor],
|
|
// Pin to the trailing edge of the content view.
|
|
[bgView.trailingAnchor constraintEqualToAnchor:titlebarContainer.trailingAnchor],
|
|
// Give it a fixed height
|
|
[bgView.bottomAnchor constraintEqualToAnchor:contentView.topAnchor]
|
|
]];
|
|
[bgView release];
|
|
#undef tag
|
|
}
|
|
|
|
static void
|
|
apply_titlebar_color_settings(_GLFWwindow *window) {
|
|
#define tc window->ns.last_applied_titlebar_settings.color
|
|
GLFWWindow *nsw = window->ns.object;
|
|
if (!window->ns.titlebar_hidden && window->decorated && tc.was_set && window->ns.last_applied_titlebar_settings.transparent) {
|
|
NSColor *titlebar_color = [NSColor colorWithSRGBRed:tc.red green:tc.green blue:tc.blue alpha:tc.alpha];
|
|
set_title_bar_background(nsw, titlebar_color);
|
|
} else clear_title_bar_background_views(nsw);
|
|
#undef tc
|
|
}
|
|
|
|
static void
|
|
apply_window_corner_curve(_GLFWwindow *window) {
|
|
if (!window || !window->decorated) return;
|
|
if (@available(macOS 26.0, *)) {
|
|
GLFWWindow *nsw = window->ns.object;
|
|
NSView *frame_view = nsw.contentView.superview;
|
|
CALayer *layer = frame_view.layer;
|
|
if (!layer) return;
|
|
layer.cornerCurve = kCACornerCurveContinuous;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
GLFWAPI void glfwCocoaSetWindowChrome(GLFWwindow *w, unsigned int color, bool use_system_color, unsigned int system_color, int background_blur, unsigned int hide_window_decorations, bool show_text_in_titlebar, int color_space, float background_opacity, bool resizable) { @autoreleasepool {
|
|
_GLFWwindow* window = (_GLFWwindow*)w;
|
|
if (window->ns.layer_shell.is_active) return;
|
|
GLFWWindow *nsw = window->ns.object;
|
|
const bool is_transparent = _glfwPlatformFramebufferTransparent(window);
|
|
if (!is_transparent) { background_opacity = 1.0; background_blur = 0; }
|
|
NSColor *window_background = [NSColor windowBackgroundColor];
|
|
if (background_opacity < 1.0) {
|
|
// use a clear color (fully transparent) so that the final color is just the color from the surface.
|
|
// prevent blurring of shadows at window corners with desktop background by setting a low alpha background
|
|
window_background = background_blur > 0 ? [NSColor colorWithWhite: 0 alpha: 0.001f] : [NSColor clearColor];
|
|
}
|
|
NSAppearance *appearance = nil;
|
|
#define tc window->ns.last_applied_titlebar_settings.color
|
|
tc.was_set = false;
|
|
window->ns.last_applied_titlebar_settings.transparent = false;
|
|
const NSWindowStyleMask current_style_mask = [nsw styleMask];
|
|
const bool in_fullscreen = ((current_style_mask & NSWindowStyleMaskFullScreen) != 0) || window->ns.in_traditional_fullscreen;
|
|
NSAppearance *light_appearance = is_transparent ? [NSAppearance appearanceNamed:NSAppearanceNameVibrantLight] : [NSAppearance appearanceNamed:NSAppearanceNameAqua];
|
|
NSAppearance *dark_appearance = is_transparent ? [NSAppearance appearanceNamed:NSAppearanceNameVibrantDark] : [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
|
|
if (use_system_color) {
|
|
switch (system_color) {
|
|
case 1:
|
|
appearance = light_appearance; break;
|
|
case 2:
|
|
appearance = dark_appearance; break;
|
|
}
|
|
} else {
|
|
tc.red = ((color >> 16) & 0xFF) / 255.0;
|
|
tc.green = ((color >> 8) & 0xFF) / 255.0;
|
|
tc.blue = (color & 0xFF) / 255.0;
|
|
tc.alpha = background_opacity;
|
|
tc.was_set = true;
|
|
double luma = 0.2126 * tc.red + 0.7152 * tc.green + 0.0722 * tc.blue;
|
|
appearance = luma < 0.5 ? dark_appearance : light_appearance;
|
|
window->ns.last_applied_titlebar_settings.transparent = true;
|
|
}
|
|
[nsw setBackgroundColor:window_background];
|
|
[nsw setAppearance:appearance];
|
|
_glfwPlatformSetWindowBlur(window, background_blur);
|
|
bool has_shadow = false;
|
|
const char *decorations_desc = "full";
|
|
window->ns.titlebar_hidden = false;
|
|
switch (hide_window_decorations) {
|
|
case 1:
|
|
decorations_desc = "none";
|
|
window->decorated = false;
|
|
break;
|
|
case 2:
|
|
decorations_desc = "no-titlebar";
|
|
window->decorated = true;
|
|
has_shadow = true;
|
|
window->ns.last_applied_titlebar_settings.transparent = true;
|
|
window->ns.titlebar_hidden = true;
|
|
show_text_in_titlebar = false;
|
|
break;
|
|
case 4:
|
|
decorations_desc = "no-titlebar-and-no-corners";
|
|
window->decorated = false;
|
|
has_shadow = true;
|
|
break;
|
|
default:
|
|
window->decorated = true;
|
|
has_shadow = true;
|
|
break;
|
|
}
|
|
// shadow causes burn-in/ghosting because cocoa doesnt invalidate it on OS window resize/minimize/restore.
|
|
// https://github.com/kovidgoyal/kitty/issues/6439
|
|
if (is_transparent) has_shadow = false;
|
|
bool hide_titlebar_buttons = !in_fullscreen && window->ns.titlebar_hidden;
|
|
[nsw setTitlebarAppearsTransparent:window->ns.last_applied_titlebar_settings.transparent];
|
|
[nsw setHasShadow:has_shadow];
|
|
[nsw setTitleVisibility:(show_text_in_titlebar) ? NSWindowTitleVisible : NSWindowTitleHidden];
|
|
NSColorSpace *cs = nil;
|
|
switch (color_space) {
|
|
case SRGB_COLORSPACE: cs = [NSColorSpace sRGBColorSpace]; break;
|
|
case DISPLAY_P3_COLORSPACE: cs = [NSColorSpace displayP3ColorSpace]; break;
|
|
case DEFAULT_COLORSPACE: cs = nil; break; // using deviceRGBColorSpace causes a hang when transitioning to fullscreen
|
|
}
|
|
window->resizable = resizable;
|
|
debug(
|
|
"Window Chrome state:\n\tbackground: %s\n\tappearance: %s color_space: %s\n\t"
|
|
"blur: %d has_shadow: %d resizable: %d decorations: %s (%d)\n\t"
|
|
"titlebar_transparent: %d titlebar_color_set: %d title_visibility: %d hidden: %d buttons_hidden: %d"
|
|
"\n",
|
|
window_background ? [window_background.description UTF8String] : "<nil>",
|
|
appearance ? [appearance.name UTF8String] : "<nil>",
|
|
cs ? (cs.localizedName ? [cs.localizedName UTF8String] : [cs.description UTF8String]) : "<nil>",
|
|
background_blur, has_shadow, resizable, decorations_desc, window->decorated,
|
|
window->ns.last_applied_titlebar_settings.transparent, tc.was_set,
|
|
show_text_in_titlebar, window->ns.titlebar_hidden, hide_titlebar_buttons
|
|
);
|
|
[nsw setColorSpace:cs];
|
|
[[nsw standardWindowButton: NSWindowCloseButton] setHidden:hide_titlebar_buttons];
|
|
[[nsw standardWindowButton: NSWindowMiniaturizeButton] setHidden:hide_titlebar_buttons];
|
|
[[nsw standardWindowButton: NSWindowZoomButton] setHidden:hide_titlebar_buttons];
|
|
// Apple throws a hissy fit if one attempts to clear the value of NSWindowStyleMaskFullScreen outside of a full screen transition
|
|
// event. See https://github.com/kovidgoyal/kitty/issues/7106
|
|
NSWindowStyleMask fsmask = current_style_mask & NSWindowStyleMaskFullScreen;
|
|
window->ns.pre_full_screen_style_mask = getStyleMask(window);
|
|
NSWindowStyleMask desired_mask;
|
|
if (in_fullscreen && window->ns.in_traditional_fullscreen) {
|
|
desired_mask = NSWindowStyleMaskBorderless;
|
|
} else {
|
|
desired_mask = window->ns.pre_full_screen_style_mask | fsmask;
|
|
}
|
|
// Only call setStyleMask: when the mask actually changes. Redundant
|
|
// calls can trigger macOS to reposition the window (#9572).
|
|
if (desired_mask != current_style_mask) {
|
|
[nsw setStyleMask:desired_mask];
|
|
}
|
|
#undef tc
|
|
apply_titlebar_color_settings(window);
|
|
apply_window_corner_curve(window);
|
|
|
|
// HACK: Changing the style mask can cause the first responder to be cleared
|
|
[nsw makeFirstResponder:window->ns.view];
|
|
window->ns.notch_cover_color = color;
|
|
window->ns.notch_cover_opacity = background_opacity;
|
|
if (window->ns.notch_cover_window) _glfwUpdateNotchCover(window);
|
|
}}
|
|
|
|
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);
|
|
}
|
|
|
|
GLFWAPI bool glfwIsLayerShellSupported(void) { return true; }
|
|
|
|
GLFWAPI void
|
|
glfwCocoaCycleThroughOSWindows(bool backwards) {
|
|
NSArray *allWindows = [NSApp windows];
|
|
if (allWindows.count < 2) return;
|
|
NSMutableArray<NSWindow *> *filteredWindows = [NSMutableArray array];
|
|
for (NSWindow *window in allWindows) {
|
|
NSRect windowFrame = [window frame];
|
|
// Exclude zero size windows which are likely zombie windows from the Tahoe bug
|
|
// if ([obj isMemberOfClass:[MyClass class]]) {
|
|
if (
|
|
windowFrame.size.width > 0 && windowFrame.size.height > 0 && \
|
|
!window.isMiniaturized && window.isVisible && \
|
|
[window isMemberOfClass:[GLFWWindow class]]
|
|
) [filteredWindows addObject:window];
|
|
}
|
|
if (filteredWindows.count < 2) return;
|
|
[filteredWindows sortUsingComparator:^NSComparisonResult(NSWindow *a, NSWindow *b) {
|
|
return [@(a.windowNumber) compare:@(b.windowNumber)];
|
|
}];
|
|
NSWindow *keyWindow = [NSApp keyWindow];
|
|
NSUInteger index = [filteredWindows indexOfObject:keyWindow];
|
|
NSUInteger nextIndex = 0;
|
|
if (index != NSNotFound) {
|
|
if (backwards) {
|
|
nextIndex = (index == 0) ? [filteredWindows count] - 1 : index - 1;
|
|
} else nextIndex = (index + 1) % filteredWindows.count;
|
|
}
|
|
NSWindow *nextWindow = filteredWindows[nextIndex];
|
|
[nextWindow makeKeyAndOrderFront:nil];
|
|
}
|
|
|
|
|
|
GLFWAPI void
|
|
glfwCocoaRegisterMIMETypes(GLFWwindow *window, const char **mimes, size_t count) {
|
|
_GLFWwindow *w = (_GLFWwindow*)window;
|
|
NSArray *currentTypes = [w->ns.view registeredDraggedTypes];
|
|
NSMutableArray *updatedTypes = [NSMutableArray arrayWithArray:currentTypes];
|
|
for (size_t i = 0; i < count; i++) {
|
|
NSString *uti = mime_to_uti(mimes[i]);
|
|
if (![updatedTypes containsObject:uti]) [updatedTypes addObject:uti];
|
|
}
|
|
[w->ns.view registerForDraggedTypes:updatedTypes];
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
////// 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];
|
|
}
|
|
|
|
// Drag source implementation {{{
|
|
|
|
// Forward declarations for drag-finish helpers used in GLFWDraggingSource methods
|
|
static void fire_drag_finished(void);
|
|
static void schedule_drag_finish_timer(void);
|
|
static GLFWid drag_finish_window_id = 0;
|
|
static GLFWDragOperationType drag_finish_action = 0;
|
|
static NSTimer *drag_finish_timer = nil;
|
|
static NSMutableArray<GLFWFilePromiseProviderDelegate*> *file_promise_providers = nil;
|
|
#define DRAG_FINISH_TIMEOUT_SECONDS 2.0
|
|
|
|
@implementation GLFWDraggingSource
|
|
- (NSDragOperation)draggingSession:(NSDraggingSession*)session
|
|
sourceOperationMaskForDraggingContext:(NSDraggingContext)context
|
|
{
|
|
(void)session; (void)context;
|
|
// Return the operation based on the stored drag operations bitfield
|
|
NSDragOperation ops = NSDragOperationCopy;
|
|
int q = _glfw.drag.operations;
|
|
if (q & GLFW_DRAG_OPERATION_COPY) ops |= NSDragOperationCopy;
|
|
if (q & GLFW_DRAG_OPERATION_MOVE) ops |= NSDragOperationMove;
|
|
if (q & GLFW_DRAG_OPERATION_GENERIC) ops |= NSDragOperationGeneric;
|
|
return ops;
|
|
}
|
|
|
|
- (void)draggingSession:(NSDraggingSession *)session
|
|
willBeginAtPoint:(NSPoint)screenPoint
|
|
{
|
|
(void)session;
|
|
start_point = screenPoint;
|
|
}
|
|
|
|
- (void)draggingSession:(NSDraggingSession *)session
|
|
movedToPoint:(NSPoint)screenPoint
|
|
{
|
|
(void)session;
|
|
current_point = screenPoint;
|
|
}
|
|
|
|
|
|
- (void)draggingSession:(NSDraggingSession *)session
|
|
endedAtPoint:(NSPoint)screenPoint
|
|
operation:(NSDragOperation)operation
|
|
{
|
|
(void)session;
|
|
_glfwPlatformFreeDragSourceData();
|
|
_GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id);
|
|
if (window) {
|
|
GLFWDragEvent ev = {0};
|
|
switch(operation) {
|
|
case NSDragOperationCopy: case NSDragOperationLink: ev.action = GLFW_DRAG_OPERATION_COPY; break;
|
|
case NSDragOperationMove: case NSDragOperationDelete: ev.action = GLFW_DRAG_OPERATION_MOVE; break;
|
|
case NSDragOperationNone: break;
|
|
default: ev.action = GLFW_DRAG_OPERATION_GENERIC; break;
|
|
}
|
|
switch (operation) {
|
|
case NSDragOperationDelete: ev.type = GLFW_DRAG_CANCELLED; break;
|
|
case NSDragOperationNone: {
|
|
NSEvent *currentEvent = [NSApp currentEvent];
|
|
if (currentEvent && currentEvent.type == NSEventTypeKeyDown && currentEvent.keyCode == 53) {
|
|
ev.type = GLFW_DRAG_CANCELLED;
|
|
} else {
|
|
ev.type = GLFW_DRAG_DROPPED;
|
|
ev.drop_maybe_a_cancel = true;
|
|
}
|
|
} break;
|
|
default:
|
|
ev.type = GLFW_DRAG_DROPPED; break;
|
|
}
|
|
_glfwInputDragSourceRequest(window, &ev);
|
|
if (ev.type == GLFW_DRAG_DROPPED) {
|
|
drag_finish_window_id = _glfw.drag.window_id;
|
|
drag_finish_action = ev.action;
|
|
if (operation == NSDragOperationNone || !file_promise_providers || [file_promise_providers count] == 0) {
|
|
fire_drag_finished();
|
|
} else {
|
|
schedule_drag_finish_timer();
|
|
}
|
|
} else {
|
|
if (operation == NSDragOperationNone) _glfwFreeDragSourceData();
|
|
}
|
|
}
|
|
}
|
|
@end
|
|
|
|
|
|
static void
|
|
fire_drag_finished(void) {
|
|
if (drag_finish_timer) {
|
|
[drag_finish_timer invalidate];
|
|
drag_finish_timer = nil;
|
|
}
|
|
if (!drag_finish_window_id) return;
|
|
GLFWid wid = drag_finish_window_id;
|
|
GLFWDragOperationType action = drag_finish_action;
|
|
drag_finish_window_id = 0;
|
|
drag_finish_action = 0;
|
|
_GLFWwindow *window = _glfwWindowForId(wid);
|
|
if (window) {
|
|
GLFWDragEvent ev = {.type=GLFW_DRAG_FINSHED, .action=action};
|
|
_glfwInputDragSourceRequest(window, &ev);
|
|
}
|
|
_glfwFreeDragSourceData();
|
|
}
|
|
|
|
static void
|
|
schedule_drag_finish_timer(void) {
|
|
if (drag_finish_timer) {
|
|
[drag_finish_timer invalidate];
|
|
drag_finish_timer = nil;
|
|
}
|
|
drag_finish_timer = [NSTimer scheduledTimerWithTimeInterval:DRAG_FINISH_TIMEOUT_SECONDS repeats:NO block:^(NSTimer *t UNUSED) {
|
|
drag_finish_timer = nil;
|
|
fire_drag_finished();
|
|
}];
|
|
}
|
|
|
|
static int
|
|
set_image_for_dragging_item(NSDraggingItem *draggingItem, const GLFWimage *thumbnail, NSWindow *window) {
|
|
CGFloat scaleFactor = 1.0;
|
|
[draggingItem setDraggingFrame:NSMakeRect(0, 0, 32, 32) contents:nil];
|
|
if (_glfw.ns.drag_image) [_glfw.ns.drag_image release];
|
|
_glfw.ns.drag_image = nil;
|
|
if (thumbnail && thumbnail->pixels && window) {
|
|
scaleFactor = [window backingScaleFactor];
|
|
if (scaleFactor == 0) scaleFactor = [NSScreen mainScreen].backingScaleFactor;
|
|
unsigned height = thumbnail->height + 20; // add empty padding at bottom
|
|
NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc]
|
|
initWithBitmapDataPlanes:NULL
|
|
pixelsWide:thumbnail->width
|
|
pixelsHigh:height
|
|
bitsPerSample:8
|
|
samplesPerPixel:4
|
|
hasAlpha:YES
|
|
isPlanar:NO
|
|
colorSpaceName:NSDeviceRGBColorSpace
|
|
bytesPerRow:thumbnail->width * 4
|
|
bitsPerPixel:32];
|
|
if (!imageRep) return ENOMEM;
|
|
memcpy([imageRep bitmapData], thumbnail->pixels, thumbnail->width * thumbnail->height * 4);
|
|
NSSize pointSize = NSMakeSize(thumbnail->width / scaleFactor, height / scaleFactor);
|
|
[imageRep setSize:pointSize];
|
|
_glfw.ns.drag_image = [[NSImage alloc] initWithSize:pointSize];
|
|
if (!_glfw.ns.drag_image) { [imageRep release]; return ENOMEM; }
|
|
[_glfw.ns.drag_image addRepresentation:imageRep];
|
|
[imageRep release];
|
|
if (!_glfw.ns.drag_session) {
|
|
_GLFWwindow *glfw_window = _glfwWindowForId(_glfw.drag.window_id);
|
|
if (glfw_window && glfw_window->ns.view) {
|
|
NSRect contentRect = [glfw_window->ns.view frame];
|
|
NSPoint cursor = NSMakePoint(glfw_window->virtualCursorPosX,
|
|
contentRect.size.height - glfw_window->virtualCursorPosY);
|
|
[draggingItem setDraggingFrame:NSMakeRect(cursor.x, cursor.y, pointSize.width, pointSize.height) contents:nil];
|
|
} else {
|
|
[draggingItem setDraggingFrame:NSMakeRect(0, 0, pointSize.width, pointSize.height) contents:nil];
|
|
}
|
|
}
|
|
[draggingItem setImageComponentsProvider:^NSArray<NSDraggingImageComponent *> * _Nonnull{
|
|
NSDraggingImageComponent *icon = [NSDraggingImageComponent draggingImageComponentWithKey:NSDraggingImageComponentIconKey];
|
|
NSImage *image = _glfw.ns.drag_image;
|
|
icon.contents = image;
|
|
icon.frame = NSMakeRect(0, 0, pointSize.width, pointSize.height);
|
|
return @[icon];
|
|
}];
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
add_drag_item(_GLFWwindow *window, NSMutableArray<NSDraggingItem*>* dragItems, NSDraggingItem* dragItem, const GLFWimage *thumbnail) {
|
|
if (dragItems.count == 0 && thumbnail && thumbnail->pixels) {
|
|
int err = set_image_for_dragging_item(dragItem, thumbnail, window->ns.object);
|
|
if (err) return err;
|
|
} else {
|
|
[dragItem setDraggingFrame:NSMakeRect(0, 0, 32, 32) contents:nil];
|
|
}
|
|
[dragItems addObject:dragItem];
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
add_uri_list_drag_items(_GLFWwindow *window, NSMutableArray<NSDraggingItem*>* dragItems, const char *uri_list, size_t uri_list_sz, bool use_promises, const GLFWimage *thumbnail) {
|
|
NSString *input = [[[NSString alloc] initWithBytes:uri_list length:uri_list_sz encoding:NSUTF8StringEncoding] autorelease];
|
|
NSArray *lines = [input componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
|
|
id dragItem; int count = 0; char buf[256];
|
|
for (NSString *rawLine in lines) {
|
|
NSString *line = [rawLine stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
|
if (line.length == 0 || [line hasPrefix:@"#"]) continue;
|
|
NSURL *url = [NSURL URLWithString:line];
|
|
if (use_promises && [[url scheme] caseInsensitiveCompare:@"file"] == NSOrderedSame) {
|
|
NSString *extension = [url pathExtension];
|
|
UTType *type = [UTType typeWithFilenameExtension:extension];
|
|
if (!type) type = UTTypeItem;
|
|
snprintf(buf, sizeof(buf), "text/uri-list:%d", count);
|
|
GLFWFilePromiseProviderDelegate* delegate = [[[GLFWFilePromiseProviderDelegate alloc]
|
|
initWithWindow:window mimeType:buf instanceId:_glfw.drag.instance_id] autorelease];
|
|
NSFilePromiseProvider *provider = [[[NSFilePromiseProvider alloc]
|
|
initWithFileType:type.identifier delegate:delegate] autorelease];
|
|
// Store the delegate in the provider's user info so it's retained
|
|
provider.userInfo = delegate;
|
|
dragItem = provider;
|
|
} else dragItem = [[[NSDraggingItem alloc] initWithPasteboardWriter:url] autorelease];
|
|
int err = add_drag_item(window, dragItems, dragItem, thumbnail);
|
|
if (err) return err;
|
|
count++;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
add_drag_items(_GLFWwindow *window, NSMutableArray<NSDraggingItem*>* dragItems, GLFWDragSourceItem *mime_item, const GLFWimage *thumbnail) {
|
|
// URI list items get added directly to the clipboard for use by native apps since macOS does not support
|
|
// the text/uri-list MIME type, however this type remains as a private
|
|
// kitty type to enable kitty to kitty DnD for remote clients.
|
|
bool is_uri_list = false;
|
|
if (strcmp(mime_item->mime_type, "text/uri-list") == 0 && mime_item->optional_data && mime_item->data_size) {
|
|
is_uri_list = true;
|
|
int err = add_uri_list_drag_items(window, dragItems, mime_item->optional_data, mime_item->data_size, mime_item->is_remote_client, thumbnail);
|
|
if (err != 0) return err;
|
|
}
|
|
NSString* utiString = mime_to_uti(mime_item->mime_type);
|
|
id w;
|
|
// remote client URI list must be set a file promise so that the kitty drag
|
|
// source code receives a request for it and can then fetch the remote
|
|
// files.
|
|
if (mime_item->optional_data && !(is_uri_list && mime_item->is_remote_client)) {
|
|
NSPasteboardItem *pbItem = [[[NSPasteboardItem alloc] init] autorelease];
|
|
NSData *data = [NSData dataWithBytes:mime_item->optional_data length:mime_item->data_size];
|
|
[pbItem setData:data forType:utiString];
|
|
w = pbItem;
|
|
} else {
|
|
// Create file promise provider with our delegate
|
|
GLFWFilePromiseProviderDelegate* delegate = [[[GLFWFilePromiseProviderDelegate alloc]
|
|
initWithWindow:window mimeType:mime_item->mime_type instanceId:_glfw.drag.instance_id] autorelease];
|
|
NSFilePromiseProvider *provider = [[[NSFilePromiseProvider alloc]
|
|
initWithFileType:utiString delegate:delegate] autorelease];
|
|
// Store the delegate in the provider's user info so it's retained
|
|
provider.userInfo = delegate;
|
|
w = provider;
|
|
}
|
|
NSDraggingItem* dragItem = [[[NSDraggingItem alloc] initWithPasteboardWriter:w] autorelease];
|
|
return add_drag_item(window, dragItems, dragItem, thumbnail);
|
|
}
|
|
|
|
|
|
int
|
|
_glfwPlatformStartDrag(_GLFWwindow* window, const GLFWimage* thumbnail) {@autoreleasepool{
|
|
if (file_promise_providers) {
|
|
for (NSInteger i = [file_promise_providers count] - 1; i >= 0; i--) {
|
|
GLFWFilePromiseProviderDelegate* d = file_promise_providers[i];
|
|
[d end_transfer:EINVAL];
|
|
}
|
|
}
|
|
// Obtain the event and view early so we can position the drag image relative to the cursor
|
|
NSEvent* event = [NSApp currentEvent];
|
|
if (!event || ([event type] != NSEventTypeLeftMouseDown &&
|
|
[event type] != NSEventTypeLeftMouseDragged)) {
|
|
// Create a synthetic left mouse down event using stored cursor position
|
|
// Convert window coordinates to screen coordinates
|
|
NSRect contentRect = [window->ns.view frame];
|
|
NSPoint windowPos = NSMakePoint(window->virtualCursorPosX,
|
|
contentRect.size.height - window->virtualCursorPosY);
|
|
|
|
event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDown
|
|
location:windowPos
|
|
modifierFlags:0
|
|
timestamp:[[NSProcessInfo processInfo] systemUptime]
|
|
windowNumber:[window->ns.object windowNumber]
|
|
context:nil
|
|
eventNumber:0
|
|
clickCount:1
|
|
pressure:1.0];
|
|
}
|
|
|
|
if (!event) return EIO;
|
|
GLFWContentView *v = window->ns.view;
|
|
NSMutableArray<NSDraggingItem*>* dragItems = [[[NSMutableArray alloc] init] autorelease];
|
|
for (size_t i = 0; i < _glfw.drag.item_count; i++) {
|
|
int err = add_drag_items(window, dragItems, _glfw.drag.items + i, thumbnail);
|
|
if (err) return err;
|
|
}
|
|
|
|
NSDraggingSession *s = [v beginDraggingSessionWithItems:dragItems event:event source:[v draggingSource]];
|
|
_glfw.ns.drag_session = [s retain];
|
|
_glfw.ns.drag_view = [v retain];
|
|
return 0;
|
|
}}
|
|
|
|
|
|
@implementation GLFWFilePromiseProviderDelegate
|
|
|
|
- (void)end_transfer_with_error:(NSError*)err {
|
|
if (err && file_url) {
|
|
NSError *error;
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
[fileManager removeItemAtURL:file_url error:&error];
|
|
}
|
|
if (file_handle) [file_handle release];
|
|
file_handle = nil;
|
|
if (completion_handler) {
|
|
completion_handler(err);
|
|
Block_release(completion_handler);
|
|
completion_handler = nil;
|
|
}
|
|
[file_promise_providers removeObject:self];
|
|
if (drag_finish_window_id) {
|
|
if (!file_promise_providers || [file_promise_providers count] == 0) {
|
|
fire_drag_finished();
|
|
} else {
|
|
schedule_drag_finish_timer();
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)end_transfer:(int)errorCode {
|
|
[self end_transfer_with_error:errorCode ? [NSError errorWithDomain:NSPOSIXErrorDomain code:errorCode userInfo:nil] : nil];
|
|
}
|
|
|
|
- (bool)is_mimetype:(const char*)q { return strcmp(q, mimeType) == 0; }
|
|
|
|
- (void)request_drag_data {
|
|
if (instanceId != _glfw.drag.instance_id) { [self end_transfer:EINVAL]; return; }
|
|
_GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id);
|
|
if (!window) { [self end_transfer:EINVAL]; return; }
|
|
bool keep_going = true;
|
|
if (drag_finish_timer) {
|
|
[drag_finish_timer invalidate];
|
|
drag_finish_timer = nil;
|
|
}
|
|
while (keep_going) {
|
|
GLFWDragEvent ev = {.type=GLFW_DRAG_DATA_REQUEST, .mime_type=mimeType};
|
|
_glfwInputDragSourceRequest(window, &ev);
|
|
if (ev.err_num) {
|
|
keep_going = false;
|
|
if (ev.err_num != EAGAIN) [self end_transfer:ev.err_num];
|
|
} else {
|
|
if (ev.data_sz) {
|
|
NSData* nsData = [NSData dataWithBytes:ev.data length:ev.data_sz];
|
|
NSError* error = nil;
|
|
if (![file_handle writeData:nsData error:&error]) {
|
|
keep_going = false;
|
|
[self end_transfer_with_error:error];
|
|
}
|
|
} else {
|
|
keep_going = false;
|
|
[self end_transfer_with_error:nil];
|
|
}
|
|
_glfwInputDragSourceRequest(window, &ev);
|
|
}
|
|
}
|
|
}
|
|
|
|
- (instancetype)initWithWindow:(_GLFWwindow*)initWindow mimeType:(const char*)mime instanceId:(GLFWid) instance_id {
|
|
self = [super init];
|
|
if (self) {
|
|
windowId = initWindow ? initWindow->id : 0;
|
|
mimeType = _glfw_strdup(mime);
|
|
instanceId = instance_id;
|
|
if (file_promise_providers == nil) file_promise_providers = [NSMutableArray array];
|
|
[file_promise_providers addObject:self];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
free(mimeType); mimeType = NULL;
|
|
if (file_url) [file_url release];
|
|
file_url = nil;
|
|
[self end_transfer:EINVAL];
|
|
[super dealloc];
|
|
}
|
|
|
|
- (NSString*)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider fileNameForType:(NSString*)fileType {
|
|
(void)filePromiseProvider; (void)fileType;
|
|
// Generate a unique filename based on the MIME type
|
|
NSString* extension = @"data";
|
|
if (mimeType) {
|
|
UTType *type = [UTType typeWithMIMEType:@(mimeType)];
|
|
extension = type.preferredFilenameExtension;
|
|
}
|
|
return [NSString stringWithFormat:@"kitty-drag-source-%@.%@", [[NSUUID UUID] UUIDString], extension];
|
|
}
|
|
|
|
- (void)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider
|
|
writePromiseToURL:(NSURL*)url
|
|
completionHandler:(void (^)(NSError*))completionHandler
|
|
{
|
|
(void)filePromiseProvider;
|
|
_GLFWwindow* window = _glfwWindowForId(windowId);
|
|
if (!window || instanceId != _glfw.drag.instance_id) {
|
|
completionHandler([NSError errorWithDomain:NSPOSIXErrorDomain code:EINVAL userInfo:nil]);
|
|
return;
|
|
}
|
|
|
|
// Create the file
|
|
NSError* error = nil;
|
|
if (![[NSFileManager defaultManager] createFileAtPath:url.path contents:nil attributes:nil]) {
|
|
error = [NSError errorWithDomain:NSPOSIXErrorDomain code:EIO userInfo:nil];
|
|
completionHandler(error);
|
|
return;
|
|
}
|
|
|
|
NSFileHandle* fileHandle = [NSFileHandle fileHandleForWritingToURL:url error:&error];
|
|
if (!fileHandle) {
|
|
completionHandler(error);
|
|
NSError *error;
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
[fileManager removeItemAtURL:url error:&error];
|
|
return;
|
|
}
|
|
file_handle = fileHandle; completion_handler = completionHandler;
|
|
file_url = [url retain];
|
|
[self request_drag_data];
|
|
}
|
|
|
|
@end
|
|
|
|
void
|
|
_glfwPlatformCancelDrag(_GLFWwindow* window UNUSED) {@autoreleasepool{
|
|
if (!_glfw.drag.window_id) return;
|
|
_GLFWwindow* drag_win = _glfwWindowForId(_glfw.drag.window_id);
|
|
if (drag_win) {
|
|
GLFWDragEvent ev = {.type = GLFW_DRAG_CANCELLED};
|
|
_glfwInputDragSourceRequest(drag_win, &ev);
|
|
}
|
|
_glfwFreeDragSourceData();
|
|
}}
|
|
|
|
void
|
|
_glfwPlatformFreeDragSourceData(void) {
|
|
if (drag_finish_timer) {
|
|
[drag_finish_timer invalidate];
|
|
drag_finish_timer = nil;
|
|
}
|
|
drag_finish_window_id = 0;
|
|
drag_finish_action = 0;
|
|
if (_glfw.ns.drag_session) [_glfw.ns.drag_session release];
|
|
_glfw.ns.drag_session = nil;
|
|
if (_glfw.ns.drag_view) [_glfw.ns.drag_view release];
|
|
_glfw.ns.drag_view = nil;
|
|
if (_glfw.ns.drag_image) [_glfw.ns.drag_image release];
|
|
_glfw.ns.drag_image = nil;
|
|
}
|
|
|
|
int
|
|
_glfwPlatformChangeDragImage(const GLFWimage *thumbnail) {@autoreleasepool{
|
|
if (!_glfw.ns.drag_session || !_glfw.ns.drag_view) return 0;
|
|
_GLFWwindow *window = _glfwWindowForId(_glfw.drag.window_id);
|
|
[((NSDraggingSession*)_glfw.ns.drag_session)
|
|
enumerateDraggingItemsWithOptions:0
|
|
forView:(NSView*)_glfw.ns.drag_view
|
|
classes:@[[NSPasteboardItem class]]
|
|
searchOptions:@{}
|
|
usingBlock:^(NSDraggingItem *draggingItem, NSInteger idx, BOOL *stop) {
|
|
if (idx == 0) {
|
|
set_image_for_dragging_item(draggingItem, thumbnail, window->ns.object);
|
|
*stop = YES;
|
|
}
|
|
}];
|
|
return 0;
|
|
}}
|
|
|
|
int
|
|
_glfwPlatformDragDataReady(const char *mime_type) {
|
|
if (!file_promise_providers) return 0;
|
|
for (GLFWFilePromiseProviderDelegate *d in file_promise_providers) {
|
|
if ([d is_mimetype:mime_type]) [d request_drag_data];
|
|
}
|
|
return 0;
|
|
}
|