diff options
Diffstat (limited to 'platform/osx')
45 files changed, 7573 insertions, 2732 deletions
diff --git a/platform/osx/SCsub b/platform/osx/SCsub index 8ba106d1c2..d72a75af04 100644 --- a/platform/osx/SCsub +++ b/platform/osx/SCsub @@ -6,14 +6,21 @@ from platform_methods import run_in_subprocess import platform_osx_builders files = [ - "crash_handler_osx.mm", "os_osx.mm", + "godot_application.mm", + "godot_application_delegate.mm", + "crash_handler_osx.mm", + "osx_terminal_logger.mm", "display_server_osx.mm", + "godot_content_view.mm", + "godot_window_delegate.mm", + "godot_window.mm", + "key_mapping_osx.mm", "godot_main_osx.mm", "dir_access_osx.mm", "joypad_osx.cpp", "vulkan_context_osx.mm", - "gl_manager_osx.mm", + "gl_manager_osx_legacy.mm", ] prog = env.add_program("#bin/godot", files) diff --git a/platform/osx/crash_handler_osx.h b/platform/osx/crash_handler_osx.h index 1601bbaab6..72938e5e0a 100644 --- a/platform/osx/crash_handler_osx.h +++ b/platform/osx/crash_handler_osx.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/platform/osx/crash_handler_osx.mm b/platform/osx/crash_handler_osx.mm index 57bca7a5b9..3e640b3bf3 100644 --- a/platform/osx/crash_handler_osx.mm +++ b/platform/osx/crash_handler_osx.mm @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -33,7 +33,6 @@ #include "core/config/project_settings.h" #include "core/os/os.h" #include "core/version.h" -#include "core/version_hash.gen.h" #include "main/main.h" #include <string.h> @@ -94,10 +93,10 @@ static void handle_crash(int sig) { OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_CRASH); // Print the engine version just before, so that people are reminded to include the version in backtrace reports. - if (String(VERSION_HASH).length() != 0) { - fprintf(stderr, "Engine version: " VERSION_FULL_NAME " (" VERSION_HASH ")\n"); + if (String(VERSION_HASH).is_empty()) { + fprintf(stderr, "Engine version: %s\n", VERSION_FULL_NAME); } else { - fprintf(stderr, "Engine version: " VERSION_FULL_NAME "\n"); + fprintf(stderr, "Engine version: %s (%s)\n", VERSION_FULL_NAME, VERSION_HASH); } fprintf(stderr, "Dumping the backtrace. %s\n", msg.utf8().get_data()); char **strings = backtrace_symbols(bt_buffer, size); diff --git a/platform/osx/detect.py b/platform/osx/detect.py index c67791b340..0ff93bedb4 100644 --- a/platform/osx/detect.py +++ b/platform/osx/detect.py @@ -78,14 +78,16 @@ def configure(env): env["osxcross"] = True if env["arch"] == "arm64": - print("Building for macOS 10.15+, platform arm64.") - env.Append(CCFLAGS=["-arch", "arm64", "-mmacosx-version-min=10.15"]) - env.Append(LINKFLAGS=["-arch", "arm64", "-mmacosx-version-min=10.15"]) + print("Building for macOS 11.0+, platform arm64.") + env.Append(CCFLAGS=["-arch", "arm64", "-mmacosx-version-min=11.0"]) + env.Append(LINKFLAGS=["-arch", "arm64", "-mmacosx-version-min=11.0"]) else: print("Building for macOS 10.12+, platform x86_64.") env.Append(CCFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.12"]) env.Append(LINKFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.12"]) + env.Append(CCFLAGS=["-fobjc-arc"]) + if not "osxcross" in env: # regular native build if env["macports_clang"] != "no": mpprefix = os.environ.get("MACPORTS_PREFIX", "/opt/local") @@ -188,6 +190,8 @@ def configure(env): env.Append(CCFLAGS=["-Wno-deprecated-declarations"]) # Disable deprecation warnings env.Append(LINKFLAGS=["-framework", "OpenGL"]) + env.Append(LINKFLAGS=["-rpath", "@executable_path/../Frameworks", "-rpath", "@executable_path"]) + if env["vulkan"]: env.Append(CPPDEFINES=["VULKAN_ENABLED"]) env.Append(LINKFLAGS=["-framework", "Metal", "-framework", "QuartzCore", "-framework", "IOSurface"]) diff --git a/platform/osx/dir_access_osx.h b/platform/osx/dir_access_osx.h index a894723e64..3c66c81d4f 100644 --- a/platform/osx/dir_access_osx.h +++ b/platform/osx/dir_access_osx.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/platform/osx/dir_access_osx.mm b/platform/osx/dir_access_osx.mm index 552c33d018..d26f35e847 100644 --- a/platform/osx/dir_access_osx.mm +++ b/platform/osx/dir_access_osx.mm @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/platform/osx/display_server_osx.h b/platform/osx/display_server_osx.h index 3cc0b10c5b..eaf03d298e 100644 --- a/platform/osx/display_server_osx.h +++ b/platform/osx/display_server_osx.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -37,13 +37,13 @@ #include "servers/display_server.h" #if defined(GLES3_ENABLED) -#include "gl_manager_osx.h" -#endif +#include "gl_manager_osx_legacy.h" +#endif // GLES3_ENABLED #if defined(VULKAN_ENABLED) #include "drivers/vulkan/rendering_device_vulkan.h" #include "platform/osx/vulkan_context_osx.h" -#endif +#endif // VULKAN_ENABLED #include <AppKit/AppKit.h> #include <AppKit/NSCursor.h> @@ -59,27 +59,8 @@ class DisplayServerOSX : public DisplayServer { _THREAD_SAFE_CLASS_ public: - void _send_event(NSEvent *p_event); - NSMenu *_get_dock_menu() const; - void _menu_callback(id p_sender); - -#if defined(GLES3_ENABLED) - GLManager_OSX *gl_manager = nullptr; -#endif -#if defined(VULKAN_ENABLED) - VulkanContextOSX *context_vulkan = nullptr; - RenderingDeviceVulkan *rendering_device_vulkan = nullptr; -#endif - - const NSMenu *_get_menu_root(const String &p_menu_root) const; - NSMenu *_get_menu_root(const String &p_menu_root); - - NSMenu *apple_menu = nullptr; - NSMenu *dock_menu = nullptr; - Map<String, NSMenu *> submenu; - struct KeyEvent { - WindowID window_id; + WindowID window_id = INVALID_WINDOW_ID; unsigned int osx_state = false; bool pressed = false; bool echo = false; @@ -89,18 +70,6 @@ public: uint32_t unicode = 0; }; - struct WarpEvent { - NSTimeInterval timestamp; - NSPoint delta; - }; - - List<WarpEvent> warp_events; - NSTimeInterval last_warp = 0; - bool ignore_warp = false; - - Vector<KeyEvent> key_event_buffer; - int key_event_pos; - struct WindowData { id window_delegate; id window_object; @@ -114,8 +83,6 @@ public: Size2i max_size; Size2i size; - bool mouse_down_control = false; - bool im_active = false; Size2i im_position; @@ -138,47 +105,102 @@ public: bool no_focus = false; }; +private: +#if defined(GLES3_ENABLED) + GLManager_OSX *gl_manager = nullptr; +#endif +#if defined(VULKAN_ENABLED) + VulkanContextOSX *context_vulkan = nullptr; + RenderingDeviceVulkan *rendering_device_vulkan = nullptr; +#endif + String rendering_driver; + + NSMenu *apple_menu = nullptr; + NSMenu *dock_menu = nullptr; + Map<String, NSMenu *> submenu; + + struct WarpEvent { + NSTimeInterval timestamp; + NSPoint delta; + }; + List<WarpEvent> warp_events; + NSTimeInterval last_warp = 0; + bool ignore_warp = false; + + Vector<KeyEvent> key_event_buffer; + int key_event_pos = 0; + Point2i im_selection; String im_text; - Map<WindowID, WindowData> windows; + CGEventSourceRef event_source; + MouseMode mouse_mode = MOUSE_MODE_VISIBLE; + MouseButton last_button_state = MouseButton::NONE; + + bool drop_events = false; + bool in_dispatch_input_event = false; + + struct LayoutInfo { + String name; + String code; + }; + Vector<LayoutInfo> kbd_layouts; + int current_layout = 0; + bool keyboard_layout_dirty = true; + WindowID last_focused_window = INVALID_WINDOW_ID; WindowID window_id_counter = MAIN_WINDOW_ID; + float display_max_scale = 1.f; + Point2i origin; + bool displays_arrangement_dirty = true; - WindowID _create_window(WindowMode p_mode, VSyncMode p_vsync_mode, const Rect2i &p_rect); - void _update_window(WindowData p_wd); - void _send_window_event(const WindowData &wd, WindowEvent p_event); - static void _dispatch_input_events(const Ref<InputEvent> &p_event); - void _dispatch_input_event(const Ref<InputEvent> &p_event); - WindowID _find_window_id(id p_window); + CursorShape cursor_shape = CURSOR_ARROW; + NSCursor *cursors[CURSOR_MAX]; + Map<CursorShape, Vector<Variant>> cursors_cache; + + Map<WindowID, WindowData> windows; + const NSMenu *_get_menu_root(const String &p_menu_root) const; + NSMenu *_get_menu_root(const String &p_menu_root); + + WindowID _create_window(WindowMode p_mode, VSyncMode p_vsync_mode, const Rect2i &p_rect); + void _update_window_style(WindowData p_wd); void _set_window_per_pixel_transparency_enabled(bool p_enabled, WindowID p_window); + void _update_displays_arrangement(); Point2i _get_screens_origin() const; Point2i _get_native_screen_position(int p_screen) const; + static void _displays_arrangement_changed(CGDirectDisplayID display_id, CGDisplayChangeSummaryFlags flags, void *user_info); + static void _dispatch_input_events(const Ref<InputEvent> &p_event); + void _dispatch_input_event(const Ref<InputEvent> &p_event); void _push_input(const Ref<InputEvent> &p_event); void _process_key_events(); - void _release_pressed_events(); + void _update_keyboard_layouts(); + static void _keyboard_layout_changed(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef user_info); - String rendering_driver; + static NSCursor *_cursor_from_selector(SEL p_selector, SEL p_fallback = nil); - id autoreleasePool; - CGEventSourceRef eventSource; +public: + NSMenu *get_dock_menu() const; + void menu_callback(id p_sender); - CursorShape cursor_shape; - NSCursor *cursors[CURSOR_MAX]; - Map<CursorShape, Vector<Variant>> cursors_cache; + bool has_window(WindowID p_window) const; + WindowData &get_window(WindowID p_window); - MouseMode mouse_mode; - Point2i last_mouse_pos; - MouseButton last_button_state = MouseButton::NONE; + void send_event(NSEvent *p_event); + void send_window_event(const WindowData &p_wd, WindowEvent p_event); + void release_pressed_events(); + void get_key_modifier_state(unsigned int p_osx_state, Ref<InputEventWithModifiers> r_state) const; + void update_mouse_pos(WindowData &p_wd, NSPoint p_location_in_window); + void push_to_key_event_buffer(const KeyEvent &p_event); + void update_im_text(const Point2i &p_selection, const String &p_text); + void set_last_focused_window(WindowID p_window); - bool window_focused; - bool drop_events; - bool in_dispatch_input_event = false; + void window_update(WindowID p_window); + void window_destroy(WindowID p_window); + void window_resize(WindowID p_window, int p_width, int p_height); -public: virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; @@ -212,9 +234,10 @@ public: virtual void mouse_set_mode(MouseMode p_mode) override; virtual MouseMode mouse_get_mode() const override; + bool update_mouse_wrap(WindowData &p_wd, NSPoint &r_delta, NSPoint &r_mpos, NSTimeInterval p_timestamp); virtual void mouse_warp_to_position(const Point2i &p_to) override; virtual Point2i mouse_get_position() const override; - virtual Point2i mouse_get_absolute_position() const override; + void mouse_set_button_state(MouseButton p_state); virtual MouseButton mouse_get_button_state() const override; virtual void clipboard_set(const String &p_text) override; @@ -227,6 +250,7 @@ public: virtual float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual float screen_get_max_scale() const override; virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual Vector<int> get_window_list() const override; @@ -281,6 +305,8 @@ public: virtual WindowID get_window_at_screen_position(const Point2i &p_position) const override; + virtual int64_t window_get_native_handle(HandleType p_handle_type, WindowID p_window = MAIN_WINDOW_ID) const override; + virtual void window_attach_instance_id(ObjectID p_instance, WindowID p_window = MAIN_WINDOW_ID) override; virtual ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const override; virtual void gl_window_make_current(DisplayServer::WindowID p_window_id) override; @@ -291,6 +317,7 @@ public: virtual Point2i ime_get_selection() const override; virtual String ime_get_text() const override; + void cursor_update_shape(); virtual void cursor_set_shape(CursorShape p_shape) override; virtual CursorShape cursor_get_shape() const override; virtual void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; @@ -314,9 +341,6 @@ public: virtual void set_native_icon(const String &p_filename) override; virtual void set_icon(const Ref<Image> &p_icon) override; - virtual void console_set_visible(bool p_enabled) override; - virtual bool is_console_visible() const override; - static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); static Vector<String> get_rendering_drivers_func(); diff --git a/platform/osx/display_server_osx.mm b/platform/osx/display_server_osx.mm index fec5c98a99..2691664b96 100644 --- a/platform/osx/display_server_osx.mm +++ b/platform/osx/display_server_osx.mm @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,6 +30,11 @@ #include "display_server_osx.h" +#include "godot_content_view.h" +#include "godot_menu_item.h" +#include "godot_window.h" +#include "godot_window_delegate.h" +#include "key_mapping_osx.h" #include "os_osx.h" #include "core/io/marshalls.h" @@ -47,222 +52,122 @@ #if defined(GLES3_ENABLED) #include "drivers/gles3/rasterizer_gles3.h" - -#import <AppKit/NSOpenGLView.h> #endif #if defined(VULKAN_ENABLED) #include "servers/rendering/renderer_rd/renderer_compositor_rd.h" - -#include <QuartzCore/CAMetalLayer.h> -#endif - -#ifndef NSAppKitVersionNumber10_14 -#define NSAppKitVersionNumber10_14 1671 #endif -#define DS_OSX ((DisplayServerOSX *)(DisplayServerOSX::get_singleton())) - -static bool ignore_momentum_scroll = false; - -static void _get_key_modifier_state(unsigned int p_osx_state, Ref<InputEventWithModifiers> r_state) { - r_state->set_shift_pressed((p_osx_state & NSEventModifierFlagShift)); - r_state->set_ctrl_pressed((p_osx_state & NSEventModifierFlagControl)); - r_state->set_alt_pressed((p_osx_state & NSEventModifierFlagOption)); - r_state->set_meta_pressed((p_osx_state & NSEventModifierFlagCommand)); -} - -static Vector2i _get_mouse_pos(DisplayServerOSX::WindowData &p_wd, NSPoint p_locationInWindow) { - const NSRect contentRect = [p_wd.window_view frame]; - const float scale = DS_OSX->screen_get_max_scale(); - p_wd.mouse_pos.x = p_locationInWindow.x * scale; - p_wd.mouse_pos.y = (contentRect.size.height - p_locationInWindow.y) * scale; - DS_OSX->last_mouse_pos = p_wd.mouse_pos; - Input::get_singleton()->set_mouse_position(p_wd.mouse_pos); - return p_wd.mouse_pos; -} - -static void _push_to_key_event_buffer(const DisplayServerOSX::KeyEvent &p_event) { - Vector<DisplayServerOSX::KeyEvent> &buffer = DS_OSX->key_event_buffer; - if (DS_OSX->key_event_pos >= buffer.size()) { - buffer.resize(1 + DS_OSX->key_event_pos); - } - buffer.write[DS_OSX->key_event_pos++] = p_event; -} - -static NSCursor *_cursorFromSelector(SEL selector, SEL fallback = nil) { - if ([NSCursor respondsToSelector:selector]) { - id object = [NSCursor performSelector:selector]; - if ([object isKindOfClass:[NSCursor class]]) { - return object; +const NSMenu *DisplayServerOSX::_get_menu_root(const String &p_menu_root) const { + const NSMenu *menu = nullptr; + if (p_menu_root == "") { + // Main menu. + menu = [NSApp mainMenu]; + } else if (p_menu_root.to_lower() == "_dock") { + // macOS dock menu. + menu = dock_menu; + } else { + // Submenu. + if (submenu.has(p_menu_root)) { + menu = submenu[p_menu_root]; } } - if (fallback) { - // Fallback should be a reasonable default, no need to check. - return [NSCursor performSelector:fallback]; - } - return [NSCursor arrowCursor]; -} - -/*************************************************************************/ -/* GlobalMenuItem */ -/*************************************************************************/ - -@interface GlobalMenuItem : NSObject { -@public - Callable callback; - Variant meta; - bool checkable; -} - -@end - -@implementation GlobalMenuItem -@end - -/*************************************************************************/ -/* GodotWindowDelegate */ -/*************************************************************************/ - -@interface GodotWindowDelegate : NSObject { - DisplayServerOSX::WindowID window_id; -} - -- (void)windowWillClose:(NSNotification *)notification; -- (void)setWindowID:(DisplayServerOSX::WindowID)wid; - -@end - -@implementation GodotWindowDelegate - -- (void)setWindowID:(DisplayServerOSX::WindowID)wid { - window_id = wid; -} - -- (BOOL)windowShouldClose:(id)sender { - if (!DS_OSX || !DS_OSX->windows.has(window_id)) { - return YES; + if (menu == apple_menu) { + // Do not allow to change Apple menu. + return nullptr; } - DS_OSX->_send_window_event(DS_OSX->windows[window_id], DisplayServerOSX::WINDOW_EVENT_CLOSE_REQUEST); - return NO; + return menu; } -- (void)windowWillClose:(NSNotification *)notification { - if (!DS_OSX || !DS_OSX->windows.has(window_id)) { - return; - } - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - while (wd.transient_children.size()) { - DS_OSX->window_set_transient(wd.transient_children.front()->get(), DisplayServerOSX::INVALID_WINDOW_ID); - } - - if (wd.transient_parent != DisplayServerOSX::INVALID_WINDOW_ID) { - DisplayServerOSX::WindowData &pwd = DS_OSX->windows[wd.transient_parent]; - [pwd.window_object makeKeyAndOrderFront:nil]; // Move focus back to parent. - DS_OSX->window_set_transient(window_id, DisplayServerOSX::INVALID_WINDOW_ID); - } else if ((window_id != DisplayServerOSX::MAIN_WINDOW_ID) && (DS_OSX->windows.size() == 1)) { - DisplayServerOSX::WindowData &pwd = DS_OSX->windows[DisplayServerOSX::MAIN_WINDOW_ID]; - [pwd.window_object makeKeyAndOrderFront:nil]; // Move focus back to main window if there is no parent or other windows left. - } - -#if defined(GLES3_ENABLED) - if (DS_OSX->rendering_driver == "opengl3") { - DS_OSX->gl_manager->window_destroy(window_id); - } -#endif -#ifdef VULKAN_ENABLED - if (DS_OSX->rendering_driver == "vulkan") { - DS_OSX->context_vulkan->window_destroy(window_id); +NSMenu *DisplayServerOSX::_get_menu_root(const String &p_menu_root) { + NSMenu *menu = nullptr; + if (p_menu_root == "") { + // Main menu. + menu = [NSApp mainMenu]; + } else if (p_menu_root.to_lower() == "_dock") { + // macOS dock menu. + menu = dock_menu; + } else { + // Submenu. + if (!submenu.has(p_menu_root)) { + NSMenu *n_menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:p_menu_root.utf8().get_data()]]; + submenu[p_menu_root] = n_menu; + } + menu = submenu[p_menu_root]; } -#endif - - DS_OSX->windows.erase(window_id); -} - -- (void)windowDidEnterFullScreen:(NSNotification *)notification { - if (!DS_OSX || !DS_OSX->windows.has(window_id)) { - return; + if (menu == apple_menu) { + // Do not allow to change Apple menu. + return nullptr; } - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - wd.fullscreen = true; - - [wd.window_object setContentMinSize:NSMakeSize(0, 0)]; - [wd.window_object setContentMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)]; - // Force window resize event. - [self windowDidResize:notification]; + return menu; } -- (void)windowDidExitFullScreen:(NSNotification *)notification { - if (!DS_OSX || !DS_OSX->windows.has(window_id)) { - return; - } - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - wd.fullscreen = false; - - const float scale = DS_OSX->screen_get_max_scale(); - if (wd.min_size != Size2i()) { - Size2i size = wd.min_size / scale; - [wd.window_object setContentMinSize:NSMakeSize(size.x, size.y)]; - } - if (wd.max_size != Size2i()) { - Size2i size = wd.max_size / scale; - [wd.window_object setContentMaxSize:NSMakeSize(size.x, size.y)]; - } - - if (wd.resize_disabled) { - [wd.window_object setStyleMask:[wd.window_object styleMask] & ~NSWindowStyleMaskResizable]; - } - - if (wd.on_top) { - [wd.window_object setLevel:NSFloatingWindowLevel]; - } - // Force window resize event. - [self windowDidResize:notification]; -} +DisplayServerOSX::WindowID DisplayServerOSX::_create_window(WindowMode p_mode, VSyncMode p_vsync_mode, const Rect2i &p_rect) { + WindowID id; + const float scale = screen_get_max_scale(); + { + WindowData wd; -- (void)windowDidChangeBackingProperties:(NSNotification *)notification { - if (!DisplayServerOSX::get_singleton()) { - return; - } + wd.window_delegate = [[GodotWindowDelegate alloc] init]; + ERR_FAIL_COND_V_MSG(wd.window_delegate == nil, INVALID_WINDOW_ID, "Can't create a window delegate"); + [wd.window_delegate setWindowID:window_id_counter]; - ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; + Point2i position = p_rect.position; + // OS X native y-coordinate relative to _get_screens_origin() is negative, + // Godot passes a positive value. + position.y *= -1; + position += _get_screens_origin(); - CGFloat newBackingScaleFactor = [wd.window_object backingScaleFactor]; - CGFloat oldBackingScaleFactor = [[[notification userInfo] objectForKey:@"NSBackingPropertyOldScaleFactorKey"] doubleValue]; + // initWithContentRect uses bottom-left corner of the window’s frame as origin. + wd.window_object = [[GodotWindow alloc] + initWithContentRect:NSMakeRect(position.x / scale, (position.y - p_rect.size.height) / scale, p_rect.size.width / scale, p_rect.size.height / scale) + styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable + backing:NSBackingStoreBuffered + defer:NO]; + ERR_FAIL_COND_V_MSG(wd.window_object == nil, INVALID_WINDOW_ID, "Can't create a window"); + [wd.window_object setWindowID:window_id_counter]; - if (newBackingScaleFactor != oldBackingScaleFactor) { - //Set new display scale and window size - const float scale = DS_OSX->screen_get_max_scale(); - const NSRect contentRect = [wd.window_view frame]; + wd.window_view = [[GodotContentView alloc] init]; + ERR_FAIL_COND_V_MSG(wd.window_view == nil, INVALID_WINDOW_ID, "Can't create a window view"); + [wd.window_view setWindowID:window_id_counter]; + [wd.window_view setWantsLayer:TRUE]; - wd.size.width = contentRect.size.width * scale; - wd.size.height = contentRect.size.height * scale; + [wd.window_object setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; + [wd.window_object setContentView:wd.window_view]; + [wd.window_object setDelegate:wd.window_delegate]; + [wd.window_object setAcceptsMouseMovedEvents:YES]; + [wd.window_object setRestorable:NO]; + [wd.window_object setColorSpace:[NSColorSpace sRGBColorSpace]]; - DS_OSX->_send_window_event(wd, DisplayServerOSX::WINDOW_EVENT_DPI_CHANGE); + if ([wd.window_object respondsToSelector:@selector(setTabbingMode:)]) { + [wd.window_object setTabbingMode:NSWindowTabbingModeDisallowed]; + } CALayer *layer = [wd.window_view layer]; if (layer) { layer.contentsScale = scale; } - //Force window resize event - [self windowDidResize:notification]; +#if defined(VULKAN_ENABLED) + if (context_vulkan) { + Error err = context_vulkan->window_create(window_id_counter, p_vsync_mode, wd.window_view, p_rect.size.width, p_rect.size.height); + ERR_FAIL_COND_V_MSG(err != OK, INVALID_WINDOW_ID, "Can't create a Vulkan context"); + } +#endif +#if defined(GLES3_ENABLED) + if (gl_manager) { + Error err = gl_manager->window_create(window_id_counter, wd.window_view, p_rect.size.width, p_rect.size.height); + ERR_FAIL_COND_V_MSG(err != OK, INVALID_WINDOW_ID, "Can't create an OpenGL context"); + } +#endif + id = window_id_counter++; + windows[id] = wd; } -} -- (void)windowDidResize:(NSNotification *)notification { - if (!DS_OSX || !DS_OSX->windows.has(window_id)) { - return; - } - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; + WindowData &wd = windows[id]; + window_set_mode(p_mode, id); const NSRect contentRect = [wd.window_view frame]; - - const float scale = DS_OSX->screen_get_max_scale(); wd.size.width = contentRect.size.width * scale; wd.size.height = contentRect.size.height * scale; @@ -272,1203 +177,448 @@ static NSCursor *_cursorFromSelector(SEL selector, SEL fallback = nil) { } #if defined(GLES3_ENABLED) - if (DS_OSX->rendering_driver == "opengl3") { - DS_OSX->gl_manager->window_resize(window_id, wd.size.width, wd.size.height); + if (gl_manager) { + gl_manager->window_resize(id, wd.size.width, wd.size.height); } #endif #if defined(VULKAN_ENABLED) - if (DS_OSX->rendering_driver == "vulkan") { - DS_OSX->context_vulkan->window_resize(window_id, wd.size.width, wd.size.height); + if (context_vulkan) { + context_vulkan->window_resize(id, wd.size.width, wd.size.height); } #endif - if (!wd.rect_changed_callback.is_null()) { - Variant size = Rect2i(DS_OSX->window_get_position(window_id), DS_OSX->window_get_size(window_id)); - Variant *sizep = &size; - Variant ret; - Callable::CallError ce; - wd.rect_changed_callback.call((const Variant **)&sizep, 1, ret, ce); - } + return id; } -- (void)windowDidMove:(NSNotification *)notification { - if (!DS_OSX || !DS_OSX->windows.has(window_id)) { - return; - } - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - DS_OSX->_release_pressed_events(); +void DisplayServerOSX::_update_window_style(WindowData p_wd) { + bool borderless_full = false; - if (!wd.rect_changed_callback.is_null()) { - Variant size = Rect2i(DS_OSX->window_get_position(window_id), DS_OSX->window_get_size(window_id)); - Variant *sizep = &size; - Variant ret; - Callable::CallError ce; - wd.rect_changed_callback.call((const Variant **)&sizep, 1, ret, ce); - } -} + if (p_wd.borderless) { + NSRect frameRect = [p_wd.window_object frame]; + NSRect screenRect = [[p_wd.window_object screen] frame]; -- (void)windowDidBecomeKey:(NSNotification *)notification { - if (!DS_OSX || !DS_OSX->windows.has(window_id)) { - return; + // Check if our window covers up the screen. + if (frameRect.origin.x <= screenRect.origin.x && frameRect.origin.y <= frameRect.origin.y && + frameRect.size.width >= screenRect.size.width && frameRect.size.height >= screenRect.size.height) { + borderless_full = true; + } } - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - if (DS_OSX->mouse_mode == DisplayServer::MOUSE_MODE_CAPTURED) { - const NSRect contentRect = [wd.window_view frame]; - NSRect pointInWindowRect = NSMakeRect(contentRect.size.width / 2, contentRect.size.height / 2, 0, 0); - NSPoint pointOnScreen = [[wd.window_view window] convertRectToScreen:pointInWindowRect].origin; - CGPoint lMouseWarpPos = { pointOnScreen.x, CGDisplayBounds(CGMainDisplayID()).size.height - pointOnScreen.y }; - CGWarpMouseCursorPosition(lMouseWarpPos); + if (borderless_full) { + // If the window covers up the screen set the level to above the main menu and hide on deactivate. + [p_wd.window_object setLevel:NSMainMenuWindowLevel + 1]; + [p_wd.window_object setHidesOnDeactivate:YES]; } else { - _get_mouse_pos(wd, [wd.window_object mouseLocationOutsideOfEventStream]); - Input::get_singleton()->set_mouse_position(wd.mouse_pos); - } - - DS_OSX->window_focused = true; - DS_OSX->_send_window_event(wd, DisplayServerOSX::WINDOW_EVENT_FOCUS_IN); -} - -- (void)windowDidResignKey:(NSNotification *)notification { - if (!DS_OSX || !DS_OSX->windows.has(window_id)) { - return; + // Reset these when our window is not a borderless window that covers up the screen. + if (p_wd.on_top && !p_wd.fullscreen) { + [p_wd.window_object setLevel:NSFloatingWindowLevel]; + } else { + [p_wd.window_object setLevel:NSNormalWindowLevel]; + } + [p_wd.window_object setHidesOnDeactivate:NO]; } - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - DS_OSX->window_focused = false; - - DS_OSX->_release_pressed_events(); - DS_OSX->_send_window_event(wd, DisplayServerOSX::WINDOW_EVENT_FOCUS_OUT); } -- (void)windowDidMiniaturize:(NSNotification *)notification { - if (!DS_OSX || !DS_OSX->windows.has(window_id)) { - return; - } - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - DS_OSX->window_focused = false; - - DS_OSX->_release_pressed_events(); - DS_OSX->_send_window_event(wd, DisplayServerOSX::WINDOW_EVENT_FOCUS_OUT); -} +void DisplayServerOSX::_set_window_per_pixel_transparency_enabled(bool p_enabled, WindowID p_window) { + ERR_FAIL_COND(!windows.has(p_window)); + WindowData &wd = windows[p_window]; -- (void)windowDidDeminiaturize:(NSNotification *)notification { - if (!DS_OSX || !DS_OSX->windows.has(window_id)) { + if (!OS::get_singleton()->is_layered_allowed()) { return; } - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - DS_OSX->window_focused = true; - DS_OSX->_send_window_event(wd, DisplayServerOSX::WINDOW_EVENT_FOCUS_IN); -} - -@end - -/*************************************************************************/ -/* GodotContentView */ -/*************************************************************************/ - + if (wd.layered_window != p_enabled) { + if (p_enabled) { + [wd.window_object setBackgroundColor:[NSColor clearColor]]; + [wd.window_object setOpaque:NO]; + [wd.window_object setHasShadow:NO]; + CALayer *layer = [wd.window_view layer]; + if (layer) { + [layer setBackgroundColor:[NSColor clearColor].CGColor]; + [layer setOpaque:NO]; + } #if defined(GLES3_ENABLED) -@interface GodotContentView : NSOpenGLView <NSTextInputClient> { -#else -@interface GodotContentView : NSView <NSTextInputClient> { -#endif - - DisplayServerOSX::WindowID window_id; - NSTrackingArea *trackingArea; - NSMutableAttributedString *markedText; - bool imeInputEventInProgress; -} - -- (void)cancelComposition; -- (CALayer *)makeBackingLayer; -- (BOOL)wantsUpdateLayer; -- (void)updateLayer; -- (void)setWindowID:(DisplayServerOSX::WindowID)wid; - -@end - -@implementation GodotContentView - -- (void)setWindowID:(DisplayServerOSX::WindowID)wid { - window_id = wid; -} - -+ (void)initialize { - if (self == [GodotContentView class]) { - // nothing left to do here at the moment.. - } -} - -- (CALayer *)makeBackingLayer { -#if defined(VULKAN_ENABLED) - if (DS_OSX->rendering_driver == "vulkan") { - CALayer *layer = [[CAMetalLayer class] layer]; - return layer; - } + if (gl_manager) { + gl_manager->window_set_per_pixel_transparency_enabled(p_window, true); + } #endif - return [super makeBackingLayer]; -} - -- (void)updateLayer { + wd.layered_window = true; + } else { + [wd.window_object setBackgroundColor:[NSColor colorWithCalibratedWhite:1 alpha:1]]; + [wd.window_object setOpaque:YES]; + [wd.window_object setHasShadow:YES]; + CALayer *layer = [wd.window_view layer]; + if (layer) { + [layer setBackgroundColor:[NSColor colorWithCalibratedWhite:1 alpha:1].CGColor]; + [layer setOpaque:YES]; + } #if defined(GLES3_ENABLED) - if (DS_OSX->rendering_driver == "opengl3") { - DS_OSX->gl_manager->window_update(window_id); - } + if (gl_manager) { + gl_manager->window_set_per_pixel_transparency_enabled(p_window, false); + } #endif -#if defined(VULKAN_ENABLED) - if (DS_OSX->rendering_driver == "vulkan") { - [super updateLayer]; + wd.layered_window = false; + } + NSRect frameRect = [wd.window_object frame]; + [wd.window_object setFrame:NSMakeRect(frameRect.origin.x, frameRect.origin.y, frameRect.size.width + 1, frameRect.size.height) display:YES]; + [wd.window_object setFrame:frameRect display:YES]; } -#endif } -- (BOOL)wantsUpdateLayer { - return YES; -} +void DisplayServerOSX::_update_displays_arrangement() { + origin = Point2i(); -- (id)init { - self = [super init]; - trackingArea = nil; - imeInputEventInProgress = false; - [self updateTrackingAreas]; -#if MAC_OS_X_VERSION_MIN_REQUIRED >= 101400 - [self registerForDraggedTypes:[NSArray arrayWithObject:NSPasteboardTypeFileURL]]; -#else - [self registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]]; -#endif - markedText = [[NSMutableAttributedString alloc] init]; - return self; -} - -- (void)dealloc { - [trackingArea release]; - [markedText release]; - [super dealloc]; + for (int i = 0; i < get_screen_count(); i++) { + Point2i position = _get_native_screen_position(i); + if (position.x < origin.x) { + origin.x = position.x; + } + if (position.y > origin.y) { + origin.y = position.y; + } + } + displays_arrangement_dirty = false; } -static const NSRange kEmptyRange = { NSNotFound, 0 }; - -- (BOOL)hasMarkedText { - return (markedText.length > 0); -} +Point2i DisplayServerOSX::_get_screens_origin() const { + // Returns the native top-left screen coordinate of the smallest rectangle + // that encompasses all screens. Needed in get_screen_position(), + // window_get_position, and window_set_position() + // to convert between OS X native screen coordinates and the ones expected by Godot. -- (NSRange)markedRange { - return NSMakeRange(0, markedText.length); -} + if (displays_arrangement_dirty) { + const_cast<DisplayServerOSX *>(this)->_update_displays_arrangement(); + } -- (NSRange)selectedRange { - return kEmptyRange; + return origin; } -- (void)setMarkedText:(id)aString selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange { - if ([aString isKindOfClass:[NSAttributedString class]]) { - [markedText initWithAttributedString:aString]; - } else { - [markedText initWithString:aString]; - } - if (markedText.length == 0) { - [self unmarkText]; - return; +Point2i DisplayServerOSX::_get_native_screen_position(int p_screen) const { + NSArray *screenArray = [NSScreen screens]; + if ((NSUInteger)p_screen < [screenArray count]) { + NSRect nsrect = [[screenArray objectAtIndex:p_screen] frame]; + // Return the top-left corner of the screen, for OS X the y starts at the bottom. + return Point2i(nsrect.origin.x, nsrect.origin.y + nsrect.size.height) * screen_get_max_scale(); } - ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - if (wd.im_active) { - imeInputEventInProgress = true; - DS_OSX->im_text.parse_utf8([[markedText mutableString] UTF8String]); - DS_OSX->im_selection = Point2i(selectedRange.location, selectedRange.length); + return Point2i(); +} - OS_OSX::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_OS_IME_UPDATE); +void DisplayServerOSX::_displays_arrangement_changed(CGDirectDisplayID display_id, CGDisplayChangeSummaryFlags flags, void *user_info) { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (ds) { + ds->displays_arrangement_dirty = true; } } -- (void)doCommandBySelector:(SEL)aSelector { - if ([self respondsToSelector:aSelector]) { - [self performSelector:aSelector]; - } +void DisplayServerOSX::_dispatch_input_events(const Ref<InputEvent> &p_event) { + ((DisplayServerOSX *)(get_singleton()))->_dispatch_input_event(p_event); } -- (void)unmarkText { - imeInputEventInProgress = false; - [[markedText mutableString] setString:@""]; +void DisplayServerOSX::_dispatch_input_event(const Ref<InputEvent> &p_event) { + _THREAD_SAFE_METHOD_ + if (!in_dispatch_input_event) { + in_dispatch_input_event = true; - ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; + Variant ev = p_event; + Variant *evp = &ev; + Variant ret; + Callable::CallError ce; - if (wd.im_active) { - DS_OSX->im_text = String(); - DS_OSX->im_selection = Point2i(); + Ref<InputEventFromWindow> event_from_window = p_event; + if (event_from_window.is_valid() && event_from_window->get_window_id() != INVALID_WINDOW_ID) { + // Send to a window. + if (windows.has(event_from_window->get_window_id())) { + Callable callable = windows[event_from_window->get_window_id()].input_event_callback; + if (callable.is_null()) { + return; + } + callable.call((const Variant **)&evp, 1, ret, ce); + } + } else { + // Send to all windows. + for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { + Callable callable = E->get().input_event_callback; + if (callable.is_null()) { + continue; + } + callable.call((const Variant **)&evp, 1, ret, ce); + } + } - OS_OSX::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_OS_IME_UPDATE); + in_dispatch_input_event = false; } } -- (NSArray *)validAttributesForMarkedText { - return [NSArray array]; -} - -- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange { - return nil; -} - -- (NSUInteger)characterIndexForPoint:(NSPoint)aPoint { - return 0; -} - -- (NSRect)firstRectForCharacterRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange { - ERR_FAIL_COND_V(!DS_OSX->windows.has(window_id), NSMakeRect(0, 0, 0, 0)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - const NSRect contentRect = [wd.window_view frame]; - const float scale = DS_OSX->screen_get_max_scale(); - NSRect pointInWindowRect = NSMakeRect(wd.im_position.x / scale, contentRect.size.height - (wd.im_position.y / scale) - 1, 0, 0); - NSPoint pointOnScreen = [wd.window_object convertRectToScreen:pointInWindowRect].origin; - - return NSMakeRect(pointOnScreen.x, pointOnScreen.y, 0, 0); +void DisplayServerOSX::_push_input(const Ref<InputEvent> &p_event) { + Ref<InputEvent> ev = p_event; + Input::get_singleton()->parse_input_event(ev); } -- (void)cancelComposition { - [self unmarkText]; - NSTextInputContext *currentInputContext = [NSTextInputContext currentInputContext]; - [currentInputContext discardMarkedText]; -} +void DisplayServerOSX::_process_key_events() { + Ref<InputEventKey> k; + for (int i = 0; i < key_event_pos; i++) { + const KeyEvent &ke = key_event_buffer[i]; + if (ke.raw) { + // Non IME input - no composite characters, pass events as is. + k.instantiate(); -- (void)insertText:(id)aString { - [self insertText:aString replacementRange:NSMakeRange(0, 0)]; -} + k->set_window_id(ke.window_id); + get_key_modifier_state(ke.osx_state, k); + k->set_pressed(ke.pressed); + k->set_echo(ke.echo); + k->set_keycode(ke.keycode); + k->set_physical_keycode((Key)ke.physical_keycode); + k->set_unicode(ke.unicode); -- (void)insertText:(id)aString replacementRange:(NSRange)replacementRange { - NSEvent *event = [NSApp currentEvent]; + _push_input(k); + } else { + // IME input. + if ((i == 0 && ke.keycode == Key::NONE) || (i > 0 && key_event_buffer[i - 1].keycode == Key::NONE)) { + k.instantiate(); - NSString *characters; - if ([aString isKindOfClass:[NSAttributedString class]]) { - characters = [aString string]; - } else { - characters = (NSString *)aString; - } + k->set_window_id(ke.window_id); + get_key_modifier_state(ke.osx_state, k); + k->set_pressed(ke.pressed); + k->set_echo(ke.echo); + k->set_keycode(Key::NONE); + k->set_physical_keycode(Key::NONE); + k->set_unicode(ke.unicode); - NSCharacterSet *ctrlChars = [NSCharacterSet controlCharacterSet]; - NSCharacterSet *wsnlChars = [NSCharacterSet whitespaceAndNewlineCharacterSet]; - if ([characters rangeOfCharacterFromSet:ctrlChars].length && [characters rangeOfCharacterFromSet:wsnlChars].length == 0) { - NSTextInputContext *currentInputContext = [NSTextInputContext currentInputContext]; - [currentInputContext discardMarkedText]; - [self cancelComposition]; - return; - } + _push_input(k); + } + if (ke.keycode != Key::NONE) { + k.instantiate(); - Char16String text; - text.resize([characters length] + 1); - [characters getCharacters:(unichar *)text.ptrw() range:NSMakeRange(0, [characters length])]; + k->set_window_id(ke.window_id); + get_key_modifier_state(ke.osx_state, k); + k->set_pressed(ke.pressed); + k->set_echo(ke.echo); + k->set_keycode(ke.keycode); + k->set_physical_keycode((Key)ke.physical_keycode); - String u32text; - u32text.parse_utf16(text.ptr(), text.length()); + if (i + 1 < key_event_pos && key_event_buffer[i + 1].keycode == Key::NONE) { + k->set_unicode(key_event_buffer[i + 1].unicode); + } - for (int i = 0; i < u32text.length(); i++) { - const char32_t codepoint = u32text[i]; - if ((codepoint & 0xFF00) == 0xF700) { - continue; + _push_input(k); + } } - - DisplayServerOSX::KeyEvent ke; - - ke.window_id = window_id; - ke.osx_state = [event modifierFlags]; - ke.pressed = true; - ke.echo = false; - ke.raw = false; // IME input event - ke.keycode = Key::NONE; - ke.physical_keycode = Key::NONE; - ke.unicode = codepoint; - - _push_to_key_event_buffer(ke); } - [self cancelComposition]; -} -- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender { - return NSDragOperationCopy; + key_event_pos = 0; } -- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender { - return NSDragOperationCopy; -} +void DisplayServerOSX::_update_keyboard_layouts() { + kbd_layouts.clear(); + current_layout = 0; -- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender { - ERR_FAIL_COND_V(!DS_OSX->windows.has(window_id), NO); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; + TISInputSourceRef cur_source = TISCopyCurrentKeyboardInputSource(); + NSString *cur_name = (__bridge NSString *)TISGetInputSourceProperty(cur_source, kTISPropertyLocalizedName); + CFRelease(cur_source); - if (!wd.drop_files_callback.is_null()) { - Vector<String> files; - NSPasteboard *pboard = [sender draggingPasteboard]; + // Enum IME layouts. + NSDictionary *filter_ime = @{ (NSString *)kTISPropertyInputSourceType : (NSString *)kTISTypeKeyboardInputMode }; + NSArray *list_ime = (__bridge NSArray *)TISCreateInputSourceList((__bridge CFDictionaryRef)filter_ime, false); + for (NSUInteger i = 0; i < [list_ime count]; i++) { + LayoutInfo ly; + NSString *name = (__bridge NSString *)TISGetInputSourceProperty((__bridge TISInputSourceRef)[list_ime objectAtIndex:i], kTISPropertyLocalizedName); + ly.name.parse_utf8([name UTF8String]); -#if MAC_OS_X_VERSION_MIN_REQUIRED >= 101400 - NSArray *items = pboard.pasteboardItems; - for (NSPasteboardItem *item in items) { - NSString *path = [item stringForType:NSPasteboardTypeFileURL]; - NSString *ns = [NSURL URLWithString:path].path; - char *utfs = strdup([ns UTF8String]); - String ret; - ret.parse_utf8(utfs); - free(utfs); - files.push_back(ret); - } -#else - NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType]; - for (NSString *ns in filenames) { - char *utfs = strdup([ns UTF8String]); - String ret; - ret.parse_utf8(utfs); - free(utfs); - files.push_back(ret); - } -#endif + NSArray *langs = (__bridge NSArray *)TISGetInputSourceProperty((__bridge TISInputSourceRef)[list_ime objectAtIndex:i], kTISPropertyInputSourceLanguages); + ly.code.parse_utf8([(NSString *)[langs objectAtIndex:0] UTF8String]); + kbd_layouts.push_back(ly); - Variant v = files; - Variant *vp = &v; - Variant ret; - Callable::CallError ce; - wd.drop_files_callback.call((const Variant **)&vp, 1, ret, ce); + if ([name isEqualToString:cur_name]) { + current_layout = kbd_layouts.size() - 1; + } } - return NO; -} + // Enum plain keyboard layouts. + NSDictionary *filter_kbd = @{ (NSString *)kTISPropertyInputSourceType : (NSString *)kTISTypeKeyboardLayout }; + NSArray *list_kbd = (__bridge NSArray *)TISCreateInputSourceList((__bridge CFDictionaryRef)filter_kbd, false); + for (NSUInteger i = 0; i < [list_kbd count]; i++) { + LayoutInfo ly; + NSString *name = (__bridge NSString *)TISGetInputSourceProperty((__bridge TISInputSourceRef)[list_kbd objectAtIndex:i], kTISPropertyLocalizedName); + ly.name.parse_utf8([name UTF8String]); -- (BOOL)isOpaque { - return YES; -} + NSArray *langs = (__bridge NSArray *)TISGetInputSourceProperty((__bridge TISInputSourceRef)[list_kbd objectAtIndex:i], kTISPropertyInputSourceLanguages); + ly.code.parse_utf8([(NSString *)[langs objectAtIndex:0] UTF8String]); + kbd_layouts.push_back(ly); -- (BOOL)canBecomeKeyView { - if (DS_OSX->windows.has(window_id)) { - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - if (wd.no_focus) { - return NO; + if ([name isEqualToString:cur_name]) { + current_layout = kbd_layouts.size() - 1; } } - return YES; -} - -- (BOOL)acceptsFirstResponder { - return YES; -} -- (void)cursorUpdate:(NSEvent *)event { - DisplayServer::CursorShape p_shape = DS_OSX->cursor_shape; - DS_OSX->cursor_shape = DisplayServer::CURSOR_MAX; - DS_OSX->cursor_set_shape(p_shape); + keyboard_layout_dirty = false; } -static void _mouseDownEvent(DisplayServer::WindowID window_id, NSEvent *event, MouseButton index, MouseButton mask, bool pressed) { - ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - if (pressed) { - DS_OSX->last_button_state |= mask; - } else { - DS_OSX->last_button_state &= (MouseButton)~mask; - } - - Ref<InputEventMouseButton> mb; - mb.instantiate(); - mb->set_window_id(window_id); - const Vector2 pos = _get_mouse_pos(wd, [event locationInWindow]); - _get_key_modifier_state([event modifierFlags], mb); - mb->set_button_index(index); - mb->set_pressed(pressed); - mb->set_position(pos); - mb->set_global_position(pos); - mb->set_button_mask(DS_OSX->last_button_state); - if (index == MouseButton::LEFT && pressed) { - mb->set_double_click([event clickCount] == 2); +void DisplayServerOSX::_keyboard_layout_changed(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef user_info) { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (ds) { + ds->keyboard_layout_dirty = true; } - - Input::get_singleton()->parse_input_event(mb); } -- (void)mouseDown:(NSEvent *)event { - ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - if (([event modifierFlags] & NSEventModifierFlagControl)) { - wd.mouse_down_control = true; - _mouseDownEvent(window_id, event, MouseButton::RIGHT, MouseButton::MASK_RIGHT, true); - } else { - wd.mouse_down_control = false; - _mouseDownEvent(window_id, event, MouseButton::LEFT, MouseButton::MASK_LEFT, true); +NSCursor *DisplayServerOSX::_cursor_from_selector(SEL p_selector, SEL p_fallback) { + if ([NSCursor respondsToSelector:p_selector]) { + id object = [NSCursor performSelector:p_selector]; + if ([object isKindOfClass:[NSCursor class]]) { + return object; + } } -} - -- (void)mouseDragged:(NSEvent *)event { - [self mouseMoved:event]; -} - -- (void)mouseUp:(NSEvent *)event { - ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - if (wd.mouse_down_control) { - _mouseDownEvent(window_id, event, MouseButton::RIGHT, MouseButton::MASK_RIGHT, false); - } else { - _mouseDownEvent(window_id, event, MouseButton::LEFT, MouseButton::MASK_LEFT, false); + if (p_fallback) { + // Fallback should be a reasonable default, no need to check. + return [NSCursor performSelector:p_fallback]; } + return [NSCursor arrowCursor]; } -- (void)mouseMoved:(NSEvent *)event { - ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - NSPoint delta = NSMakePoint([event deltaX], [event deltaY]); - NSPoint mpos = [event locationInWindow]; +NSMenu *DisplayServerOSX::get_dock_menu() const { + return dock_menu; +} - if (DS_OSX->ignore_warp) { - // Discard late events, before warp - if (([event timestamp]) < DS_OSX->last_warp) { - return; - } - DS_OSX->ignore_warp = false; +void DisplayServerOSX::menu_callback(id p_sender) { + if (![p_sender representedObject]) { return; } - if (DS_OSX->mouse_mode == DisplayServer::MOUSE_MODE_CONFINED || DS_OSX->mouse_mode == DisplayServer::MOUSE_MODE_CONFINED_HIDDEN) { - // Discard late events - if (([event timestamp]) < DS_OSX->last_warp) { - return; - } + GodotMenuItem *value = [p_sender representedObject]; - // Warp affects next event delta, subtract previous warp deltas - List<DisplayServerOSX::WarpEvent>::Element *F = DS_OSX->warp_events.front(); - while (F) { - if (F->get().timestamp < [event timestamp]) { - List<DisplayServerOSX::WarpEvent>::Element *E = F; - delta.x -= E->get().delta.x; - delta.y -= E->get().delta.y; - F = F->next(); - DS_OSX->warp_events.erase(E); + if (value) { + if (value->checkable) { + if ([p_sender state] == NSControlStateValueOff) { + [p_sender setState:NSControlStateValueOn]; } else { - F = F->next(); + [p_sender setState:NSControlStateValueOff]; } } - // Confine mouse position to the window, and update delta - NSRect frame = [wd.window_object frame]; - NSPoint conf_pos = mpos; - conf_pos.x = CLAMP(conf_pos.x + delta.x, 0.f, frame.size.width); - conf_pos.y = CLAMP(conf_pos.y - delta.y, 0.f, frame.size.height); - delta.x = conf_pos.x - mpos.x; - delta.y = mpos.y - conf_pos.y; - mpos = conf_pos; - - // Move mouse cursor - NSRect pointInWindowRect = NSMakeRect(conf_pos.x, conf_pos.y, 0, 0); - conf_pos = [[wd.window_view window] convertRectToScreen:pointInWindowRect].origin; - conf_pos.y = CGDisplayBounds(CGMainDisplayID()).size.height - conf_pos.y; - CGWarpMouseCursorPosition(conf_pos); - - // Save warp data - DS_OSX->last_warp = [[NSProcessInfo processInfo] systemUptime]; - DisplayServerOSX::WarpEvent ev; - ev.timestamp = DS_OSX->last_warp; - ev.delta = delta; - DS_OSX->warp_events.push_back(ev); - } - - Ref<InputEventMouseMotion> mm; - mm.instantiate(); - - mm->set_window_id(window_id); - mm->set_button_mask(DS_OSX->last_button_state); - const Vector2i pos = _get_mouse_pos(wd, mpos); - mm->set_position(pos); - mm->set_pressure([event pressure]); - if ([event subtype] == NSEventSubtypeTabletPoint) { - const NSPoint p = [event tilt]; - mm->set_tilt(Vector2(p.x, p.y)); + if (value->callback != Callable()) { + Variant tag = value->meta; + Variant *tagp = &tag; + Variant ret; + Callable::CallError ce; + value->callback.call((const Variant **)&tagp, 1, ret, ce); + } } - mm->set_global_position(pos); - mm->set_speed(Input::get_singleton()->get_last_mouse_speed()); - const Vector2i relativeMotion = Vector2i(delta.x, delta.y) * DS_OSX->screen_get_max_scale(); - mm->set_relative(relativeMotion); - _get_key_modifier_state([event modifierFlags], mm); - - Input::get_singleton()->set_mouse_position(wd.mouse_pos); - Input::get_singleton()->parse_input_event(mm); -} - -- (void)rightMouseDown:(NSEvent *)event { - _mouseDownEvent(window_id, event, MouseButton::RIGHT, MouseButton::MASK_RIGHT, true); } -- (void)rightMouseDragged:(NSEvent *)event { - [self mouseMoved:event]; +bool DisplayServerOSX::has_window(WindowID p_window) const { + return windows.has(p_window); } -- (void)rightMouseUp:(NSEvent *)event { - _mouseDownEvent(window_id, event, MouseButton::RIGHT, MouseButton::MASK_RIGHT, false); +DisplayServerOSX::WindowData &DisplayServerOSX::get_window(WindowID p_window) { + return windows[p_window]; } -- (void)otherMouseDown:(NSEvent *)event { - if ((int)[event buttonNumber] == 2) { - _mouseDownEvent(window_id, event, MouseButton::MIDDLE, MouseButton::MASK_MIDDLE, true); - } else if ((int)[event buttonNumber] == 3) { - _mouseDownEvent(window_id, event, MouseButton::MB_XBUTTON1, MouseButton::MASK_XBUTTON1, true); - } else if ((int)[event buttonNumber] == 4) { - _mouseDownEvent(window_id, event, MouseButton::MB_XBUTTON2, MouseButton::MASK_XBUTTON2, true); - } else { - return; - } -} - -- (void)otherMouseDragged:(NSEvent *)event { - [self mouseMoved:event]; -} - -- (void)otherMouseUp:(NSEvent *)event { - if ((int)[event buttonNumber] == 2) { - _mouseDownEvent(window_id, event, MouseButton::MIDDLE, MouseButton::MASK_MIDDLE, false); - } else if ((int)[event buttonNumber] == 3) { - _mouseDownEvent(window_id, event, MouseButton::MB_XBUTTON1, MouseButton::MASK_XBUTTON1, false); - } else if ((int)[event buttonNumber] == 4) { - _mouseDownEvent(window_id, event, MouseButton::MB_XBUTTON2, MouseButton::MASK_XBUTTON2, false); - } else { - return; - } -} +void DisplayServerOSX::send_event(NSEvent *p_event) { + // Special case handling of command-period, which is traditionally a special + // shortcut in macOS and doesn't arrive at our regular keyDown handler. + if ([p_event type] == NSEventTypeKeyDown) { + if (([p_event modifierFlags] & NSEventModifierFlagCommand) && [p_event keyCode] == 0x2f) { + Ref<InputEventKey> k; + k.instantiate(); -- (void)mouseExited:(NSEvent *)event { - ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; + get_key_modifier_state([p_event modifierFlags], k); + k->set_window_id(DisplayServerOSX::INVALID_WINDOW_ID); + k->set_pressed(true); + k->set_keycode(Key::PERIOD); + k->set_physical_keycode(Key::PERIOD); + k->set_echo([p_event isARepeat]); - if (DS_OSX->mouse_mode != DisplayServer::MOUSE_MODE_CAPTURED) { - DS_OSX->_send_window_event(wd, DisplayServerOSX::WINDOW_EVENT_MOUSE_EXIT); + Input::get_singleton()->parse_input_event(k); + } } } -- (void)mouseEntered:(NSEvent *)event { - ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; +void DisplayServerOSX::send_window_event(const WindowData &wd, WindowEvent p_event) { + _THREAD_SAFE_METHOD_ - if (DS_OSX->mouse_mode != DisplayServer::MOUSE_MODE_CAPTURED) { - DS_OSX->_send_window_event(wd, DisplayServerOSX::WINDOW_EVENT_MOUSE_ENTER); + if (!wd.event_callback.is_null()) { + Variant event = int(p_event); + Variant *eventp = &event; + Variant ret; + Callable::CallError ce; + wd.event_callback.call((const Variant **)&eventp, 1, ret, ce); } - - DisplayServer::CursorShape p_shape = DS_OSX->cursor_shape; - DS_OSX->cursor_shape = DisplayServer::CURSOR_MAX; - DS_OSX->cursor_set_shape(p_shape); -} - -- (void)magnifyWithEvent:(NSEvent *)event { - ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - Ref<InputEventMagnifyGesture> ev; - ev.instantiate(); - ev->set_window_id(window_id); - _get_key_modifier_state([event modifierFlags], ev); - ev->set_position(_get_mouse_pos(wd, [event locationInWindow])); - ev->set_factor([event magnification] + 1.0); - - Input::get_singleton()->parse_input_event(ev); } -- (void)viewDidChangeBackingProperties { - // nothing left to do here -} - -- (void)updateTrackingAreas { - if (trackingArea != nil) { - [self removeTrackingArea:trackingArea]; - [trackingArea release]; - } - - NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited | NSTrackingActiveInKeyWindow | NSTrackingCursorUpdate | NSTrackingInVisibleRect; - trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] options:options owner:self userInfo:nil]; - - [self addTrackingArea:trackingArea]; - [super updateTrackingAreas]; -} - -static bool isNumpadKey(unsigned int key) { - static const unsigned int table[] = { - 0x41, /* kVK_ANSI_KeypadDecimal */ - 0x43, /* kVK_ANSI_KeypadMultiply */ - 0x45, /* kVK_ANSI_KeypadPlus */ - 0x47, /* kVK_ANSI_KeypadClear */ - 0x4b, /* kVK_ANSI_KeypadDivide */ - 0x4c, /* kVK_ANSI_KeypadEnter */ - 0x4e, /* kVK_ANSI_KeypadMinus */ - 0x51, /* kVK_ANSI_KeypadEquals */ - 0x52, /* kVK_ANSI_Keypad0 */ - 0x53, /* kVK_ANSI_Keypad1 */ - 0x54, /* kVK_ANSI_Keypad2 */ - 0x55, /* kVK_ANSI_Keypad3 */ - 0x56, /* kVK_ANSI_Keypad4 */ - 0x57, /* kVK_ANSI_Keypad5 */ - 0x58, /* kVK_ANSI_Keypad6 */ - 0x59, /* kVK_ANSI_Keypad7 */ - 0x5b, /* kVK_ANSI_Keypad8 */ - 0x5c, /* kVK_ANSI_Keypad9 */ - 0x5f, /* kVK_JIS_KeypadComma */ - 0x00 - }; - for (int i = 0; table[i] != 0; i++) { - if (key == table[i]) { - return true; - } +void DisplayServerOSX::release_pressed_events() { + _THREAD_SAFE_METHOD_ + if (Input::get_singleton()) { + Input::get_singleton()->release_pressed_events(); } - return false; } -// Keyboard symbol translation table -static const Key _osx_to_godot_table[128] = { - /* 00 */ Key::A, - /* 01 */ Key::S, - /* 02 */ Key::D, - /* 03 */ Key::F, - /* 04 */ Key::H, - /* 05 */ Key::G, - /* 06 */ Key::Z, - /* 07 */ Key::X, - /* 08 */ Key::C, - /* 09 */ Key::V, - /* 0a */ Key::SECTION, /* ISO Section */ - /* 0b */ Key::B, - /* 0c */ Key::Q, - /* 0d */ Key::W, - /* 0e */ Key::E, - /* 0f */ Key::R, - /* 10 */ Key::Y, - /* 11 */ Key::T, - /* 12 */ Key::KEY_1, - /* 13 */ Key::KEY_2, - /* 14 */ Key::KEY_3, - /* 15 */ Key::KEY_4, - /* 16 */ Key::KEY_6, - /* 17 */ Key::KEY_5, - /* 18 */ Key::EQUAL, - /* 19 */ Key::KEY_9, - /* 1a */ Key::KEY_7, - /* 1b */ Key::MINUS, - /* 1c */ Key::KEY_8, - /* 1d */ Key::KEY_0, - /* 1e */ Key::BRACERIGHT, - /* 1f */ Key::O, - /* 20 */ Key::U, - /* 21 */ Key::BRACELEFT, - /* 22 */ Key::I, - /* 23 */ Key::P, - /* 24 */ Key::ENTER, - /* 25 */ Key::L, - /* 26 */ Key::J, - /* 27 */ Key::APOSTROPHE, - /* 28 */ Key::K, - /* 29 */ Key::SEMICOLON, - /* 2a */ Key::BACKSLASH, - /* 2b */ Key::COMMA, - /* 2c */ Key::SLASH, - /* 2d */ Key::N, - /* 2e */ Key::M, - /* 2f */ Key::PERIOD, - /* 30 */ Key::TAB, - /* 31 */ Key::SPACE, - /* 32 */ Key::QUOTELEFT, - /* 33 */ Key::BACKSPACE, - /* 34 */ Key::UNKNOWN, - /* 35 */ Key::ESCAPE, - /* 36 */ Key::META, - /* 37 */ Key::META, - /* 38 */ Key::SHIFT, - /* 39 */ Key::CAPSLOCK, - /* 3a */ Key::ALT, - /* 3b */ Key::CTRL, - /* 3c */ Key::SHIFT, - /* 3d */ Key::ALT, - /* 3e */ Key::CTRL, - /* 3f */ Key::UNKNOWN, /* Function */ - /* 40 */ Key::UNKNOWN, /* F17 */ - /* 41 */ Key::KP_PERIOD, - /* 42 */ Key::UNKNOWN, - /* 43 */ Key::KP_MULTIPLY, - /* 44 */ Key::UNKNOWN, - /* 45 */ Key::KP_ADD, - /* 46 */ Key::UNKNOWN, - /* 47 */ Key::NUMLOCK, /* Really KeypadClear... */ - /* 48 */ Key::VOLUMEUP, /* VolumeUp */ - /* 49 */ Key::VOLUMEDOWN, /* VolumeDown */ - /* 4a */ Key::VOLUMEMUTE, /* Mute */ - /* 4b */ Key::KP_DIVIDE, - /* 4c */ Key::KP_ENTER, - /* 4d */ Key::UNKNOWN, - /* 4e */ Key::KP_SUBTRACT, - /* 4f */ Key::UNKNOWN, /* F18 */ - /* 50 */ Key::UNKNOWN, /* F19 */ - /* 51 */ Key::EQUAL, /* KeypadEqual */ - /* 52 */ Key::KP_0, - /* 53 */ Key::KP_1, - /* 54 */ Key::KP_2, - /* 55 */ Key::KP_3, - /* 56 */ Key::KP_4, - /* 57 */ Key::KP_5, - /* 58 */ Key::KP_6, - /* 59 */ Key::KP_7, - /* 5a */ Key::UNKNOWN, /* F20 */ - /* 5b */ Key::KP_8, - /* 5c */ Key::KP_9, - /* 5d */ Key::YEN, /* JIS Yen */ - /* 5e */ Key::UNDERSCORE, /* JIS Underscore */ - /* 5f */ Key::COMMA, /* JIS KeypadComma */ - /* 60 */ Key::F5, - /* 61 */ Key::F6, - /* 62 */ Key::F7, - /* 63 */ Key::F3, - /* 64 */ Key::F8, - /* 65 */ Key::F9, - /* 66 */ Key::UNKNOWN, /* JIS Eisu */ - /* 67 */ Key::F11, - /* 68 */ Key::UNKNOWN, /* JIS Kana */ - /* 69 */ Key::F13, - /* 6a */ Key::F16, - /* 6b */ Key::F14, - /* 6c */ Key::UNKNOWN, - /* 6d */ Key::F10, - /* 6e */ Key::MENU, - /* 6f */ Key::F12, - /* 70 */ Key::UNKNOWN, - /* 71 */ Key::F15, - /* 72 */ Key::INSERT, /* Really Help... */ - /* 73 */ Key::HOME, - /* 74 */ Key::PAGEUP, - /* 75 */ Key::KEY_DELETE, - /* 76 */ Key::F4, - /* 77 */ Key::END, - /* 78 */ Key::F2, - /* 79 */ Key::PAGEDOWN, - /* 7a */ Key::F1, - /* 7b */ Key::LEFT, - /* 7c */ Key::RIGHT, - /* 7d */ Key::DOWN, - /* 7e */ Key::UP, - /* 7f */ Key::UNKNOWN, -}; - -// Translates a OS X keycode to a Godot keycode -static Key translateKey(unsigned int key) { - if (key >= 128) { - return Key::UNKNOWN; - } - - return _osx_to_godot_table[key]; -} - -// Translates a Godot keycode back to a OSX keycode -static unsigned int unmapKey(Key key) { - for (int i = 0; i <= 126; i++) { - if (_osx_to_godot_table[i] == key) { - return i; - } - } - return 127; -} - -struct _KeyCodeMap { - UniChar kchar; - Key kcode; -}; - -static const _KeyCodeMap _keycodes[55] = { - { '`', Key::QUOTELEFT }, - { '~', Key::ASCIITILDE }, - { '0', Key::KEY_0 }, - { '1', Key::KEY_1 }, - { '2', Key::KEY_2 }, - { '3', Key::KEY_3 }, - { '4', Key::KEY_4 }, - { '5', Key::KEY_5 }, - { '6', Key::KEY_6 }, - { '7', Key::KEY_7 }, - { '8', Key::KEY_8 }, - { '9', Key::KEY_9 }, - { '-', Key::MINUS }, - { '_', Key::UNDERSCORE }, - { '=', Key::EQUAL }, - { '+', Key::PLUS }, - { 'q', Key::Q }, - { 'w', Key::W }, - { 'e', Key::E }, - { 'r', Key::R }, - { 't', Key::T }, - { 'y', Key::Y }, - { 'u', Key::U }, - { 'i', Key::I }, - { 'o', Key::O }, - { 'p', Key::P }, - { '[', Key::BRACELEFT }, - { ']', Key::BRACERIGHT }, - { '{', Key::BRACELEFT }, - { '}', Key::BRACERIGHT }, - { 'a', Key::A }, - { 's', Key::S }, - { 'd', Key::D }, - { 'f', Key::F }, - { 'g', Key::G }, - { 'h', Key::H }, - { 'j', Key::J }, - { 'k', Key::K }, - { 'l', Key::L }, - { ';', Key::SEMICOLON }, - { ':', Key::COLON }, - { '\'', Key::APOSTROPHE }, - { '\"', Key::QUOTEDBL }, - { '\\', Key::BACKSLASH }, - { '#', Key::NUMBERSIGN }, - { 'z', Key::Z }, - { 'x', Key::X }, - { 'c', Key::C }, - { 'v', Key::V }, - { 'b', Key::B }, - { 'n', Key::N }, - { 'm', Key::M }, - { ',', Key::COMMA }, - { '.', Key::PERIOD }, - { '/', Key::SLASH } -}; - -static Key remapKey(unsigned int key, unsigned int state) { - if (isNumpadKey(key)) { - return translateKey(key); - } - - TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource(); - if (!currentKeyboard) { - return translateKey(key); - } - - CFDataRef layoutData = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData); - if (!layoutData) { - return translateKey(key); - } - - const UCKeyboardLayout *keyboardLayout = (const UCKeyboardLayout *)CFDataGetBytePtr(layoutData); - - UInt32 keysDown = 0; - UniChar chars[4]; - UniCharCount realLength; - - OSStatus err = UCKeyTranslate(keyboardLayout, - key, - kUCKeyActionDisplay, - (state >> 8) & 0xFF, - LMGetKbdType(), - kUCKeyTranslateNoDeadKeysBit, - &keysDown, - sizeof(chars) / sizeof(chars[0]), - &realLength, - chars); - - if (err != noErr) { - return translateKey(key); - } - - for (unsigned int i = 0; i < 55; i++) { - if (_keycodes[i].kchar == chars[0]) { - return _keycodes[i].kcode; - } - } - return translateKey(key); -} - -- (void)keyDown:(NSEvent *)event { - ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - ignore_momentum_scroll = true; - - // Ignore all input if IME input is in progress - if (!imeInputEventInProgress) { - NSString *characters = [event characters]; - NSUInteger length = [characters length]; - - if (!wd.im_active && length > 0 && keycode_has_unicode(remapKey([event keyCode], [event modifierFlags]))) { - // Fallback unicode character handler used if IME is not active - Char16String text; - text.resize([characters length] + 1); - [characters getCharacters:(unichar *)text.ptrw() range:NSMakeRange(0, [characters length])]; - - String u32text; - u32text.parse_utf16(text.ptr(), text.length()); - - for (int i = 0; i < u32text.length(); i++) { - const char32_t codepoint = u32text[i]; - - DisplayServerOSX::KeyEvent ke; - - ke.window_id = window_id; - ke.osx_state = [event modifierFlags]; - ke.pressed = true; - ke.echo = [event isARepeat]; - ke.keycode = remapKey([event keyCode], [event modifierFlags]); - ke.physical_keycode = translateKey([event keyCode]); - ke.raw = true; - ke.unicode = codepoint; - - _push_to_key_event_buffer(ke); - } - } else { - DisplayServerOSX::KeyEvent ke; - - ke.window_id = window_id; - ke.osx_state = [event modifierFlags]; - ke.pressed = true; - ke.echo = [event isARepeat]; - ke.keycode = remapKey([event keyCode], [event modifierFlags]); - ke.physical_keycode = translateKey([event keyCode]); - ke.raw = false; - ke.unicode = 0; - - _push_to_key_event_buffer(ke); - } - } - - // Pass events to IME handler - if (wd.im_active) { - [self interpretKeyEvents:[NSArray arrayWithObject:event]]; - } +void DisplayServerOSX::get_key_modifier_state(unsigned int p_osx_state, Ref<InputEventWithModifiers> r_state) const { + r_state->set_shift_pressed((p_osx_state & NSEventModifierFlagShift)); + r_state->set_ctrl_pressed((p_osx_state & NSEventModifierFlagControl)); + r_state->set_alt_pressed((p_osx_state & NSEventModifierFlagOption)); + r_state->set_meta_pressed((p_osx_state & NSEventModifierFlagCommand)); } -- (void)flagsChanged:(NSEvent *)event { - ignore_momentum_scroll = true; - - // Ignore all input if IME input is in progress - if (!imeInputEventInProgress) { - DisplayServerOSX::KeyEvent ke; - - ke.window_id = window_id; - ke.echo = false; - ke.raw = true; - - int key = [event keyCode]; - int mod = [event modifierFlags]; - - if (key == 0x36 || key == 0x37) { - if (mod & NSEventModifierFlagCommand) { - mod &= ~NSEventModifierFlagCommand; - ke.pressed = true; - } else { - ke.pressed = false; - } - } else if (key == 0x38 || key == 0x3c) { - if (mod & NSEventModifierFlagShift) { - mod &= ~NSEventModifierFlagShift; - ke.pressed = true; - } else { - ke.pressed = false; - } - } else if (key == 0x3a || key == 0x3d) { - if (mod & NSEventModifierFlagOption) { - mod &= ~NSEventModifierFlagOption; - ke.pressed = true; - } else { - ke.pressed = false; - } - } else if (key == 0x3b || key == 0x3e) { - if (mod & NSEventModifierFlagControl) { - mod &= ~NSEventModifierFlagControl; - ke.pressed = true; - } else { - ke.pressed = false; - } - } else { - return; - } - - ke.osx_state = mod; - ke.keycode = remapKey(key, mod); - ke.physical_keycode = translateKey(key); - ke.unicode = 0; - - _push_to_key_event_buffer(ke); - } +void DisplayServerOSX::update_mouse_pos(DisplayServerOSX::WindowData &p_wd, NSPoint p_location_in_window) { + const NSRect content_rect = [p_wd.window_view frame]; + const float scale = screen_get_max_scale(); + p_wd.mouse_pos.x = p_location_in_window.x * scale; + p_wd.mouse_pos.y = (content_rect.size.height - p_location_in_window.y) * scale; + Input::get_singleton()->set_mouse_position(p_wd.mouse_pos); } -- (void)keyUp:(NSEvent *)event { - ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - // Ignore all input if IME input is in progress - if (!imeInputEventInProgress) { - NSString *characters = [event characters]; - NSUInteger length = [characters length]; - - // Fallback unicode character handler used if IME is not active - if (!wd.im_active && length > 0 && keycode_has_unicode(remapKey([event keyCode], [event modifierFlags]))) { - Char16String text; - text.resize([characters length] + 1); - [characters getCharacters:(unichar *)text.ptrw() range:NSMakeRange(0, [characters length])]; - - String u32text; - u32text.parse_utf16(text.ptr(), text.length()); - - for (int i = 0; i < u32text.length(); i++) { - const char32_t codepoint = u32text[i]; - DisplayServerOSX::KeyEvent ke; - - ke.window_id = window_id; - ke.osx_state = [event modifierFlags]; - ke.pressed = false; - ke.echo = [event isARepeat]; - ke.keycode = remapKey([event keyCode], [event modifierFlags]); - ke.physical_keycode = translateKey([event keyCode]); - ke.raw = true; - ke.unicode = codepoint; - - _push_to_key_event_buffer(ke); - } - } else { - DisplayServerOSX::KeyEvent ke; - - ke.window_id = window_id; - ke.osx_state = [event modifierFlags]; - ke.pressed = false; - ke.echo = [event isARepeat]; - ke.keycode = remapKey([event keyCode], [event modifierFlags]); - ke.physical_keycode = translateKey([event keyCode]); - ke.raw = true; - ke.unicode = 0; - - _push_to_key_event_buffer(ke); - } +void DisplayServerOSX::push_to_key_event_buffer(const DisplayServerOSX::KeyEvent &p_event) { + if (key_event_pos >= key_event_buffer.size()) { + key_event_buffer.resize(1 + key_event_pos); } + key_event_buffer.write[key_event_pos++] = p_event; } -inline void sendScrollEvent(DisplayServer::WindowID window_id, MouseButton button, double factor, int modifierFlags) { - ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - MouseButton mask = mouse_button_to_mask(button); +void DisplayServerOSX::update_im_text(const Point2i &p_selection, const String &p_text) { + im_selection = p_selection; + im_text = p_text; - Ref<InputEventMouseButton> sc; - sc.instantiate(); - - sc->set_window_id(window_id); - _get_key_modifier_state(modifierFlags, sc); - sc->set_button_index(button); - sc->set_factor(factor); - sc->set_pressed(true); - sc->set_position(wd.mouse_pos); - sc->set_global_position(wd.mouse_pos); - DS_OSX->last_button_state |= (MouseButton)mask; - sc->set_button_mask(DS_OSX->last_button_state); - - Input::get_singleton()->parse_input_event(sc); - - sc.instantiate(); - sc->set_window_id(window_id); - sc->set_button_index(button); - sc->set_factor(factor); - sc->set_pressed(false); - sc->set_position(wd.mouse_pos); - sc->set_global_position(wd.mouse_pos); - DS_OSX->last_button_state &= (MouseButton)~mask; - sc->set_button_mask(DS_OSX->last_button_state); - - Input::get_singleton()->parse_input_event(sc); + OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_OS_IME_UPDATE); } -inline void sendPanEvent(DisplayServer::WindowID window_id, double dx, double dy, int modifierFlags) { - ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - Ref<InputEventPanGesture> pg; - pg.instantiate(); - - pg->set_window_id(window_id); - _get_key_modifier_state(modifierFlags, pg); - pg->set_position(wd.mouse_pos); - pg->set_delta(Vector2(-dx, -dy)); - - Input::get_singleton()->parse_input_event(pg); +void DisplayServerOSX::set_last_focused_window(WindowID p_window) { + last_focused_window = p_window; } -- (void)scrollWheel:(NSEvent *)event { - ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); - DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; - - double deltaX, deltaY; - - _get_mouse_pos(wd, [event locationInWindow]); - - deltaX = [event scrollingDeltaX]; - deltaY = [event scrollingDeltaY]; - - if ([event hasPreciseScrollingDeltas]) { - deltaX *= 0.03; - deltaY *= 0.03; +void DisplayServerOSX::window_update(WindowID p_window) { +#if defined(GLES3_ENABLED) + if (gl_manager) { + gl_manager->window_update(p_window); } +#endif +} - if ([event momentumPhase] != NSEventPhaseNone) { - if (ignore_momentum_scroll) { - return; - } - } else { - ignore_momentum_scroll = false; +void DisplayServerOSX::window_destroy(WindowID p_window) { +#if defined(GLES3_ENABLED) + if (gl_manager) { + gl_manager->window_destroy(p_window); } - - if ([event phase] != NSEventPhaseNone || [event momentumPhase] != NSEventPhaseNone) { - sendPanEvent(window_id, deltaX, deltaY, [event modifierFlags]); - } else { - if (fabs(deltaX)) { - sendScrollEvent(window_id, 0 > deltaX ? MouseButton::WHEEL_RIGHT : MouseButton::WHEEL_LEFT, fabs(deltaX * 0.3), [event modifierFlags]); - } - if (fabs(deltaY)) { - sendScrollEvent(window_id, 0 < deltaY ? MouseButton::WHEEL_UP : MouseButton::WHEEL_DOWN, fabs(deltaY * 0.3), [event modifierFlags]); - } +#endif +#ifdef VULKAN_ENABLED + if (context_vulkan) { + context_vulkan->window_destroy(p_window); } +#endif + windows.erase(p_window); } -@end - -/*************************************************************************/ -/* GodotWindow */ -/*************************************************************************/ - -@interface GodotWindow : NSWindow { -} - -@end - -@implementation GodotWindow - -- (BOOL)canBecomeKeyWindow { - // Required for NSBorderlessWindowMask windows - for (Map<DisplayServer::WindowID, DisplayServerOSX::WindowData>::Element *E = DS_OSX->windows.front(); E; E = E->next()) { - if (E->get().window_object == self) { - if (E->get().no_focus) { - return NO; - } - } +void DisplayServerOSX::window_resize(WindowID p_window, int p_width, int p_height) { +#if defined(GLES3_ENABLED) + if (gl_manager) { + gl_manager->window_resize(p_window, p_width, p_height); } - return YES; -} - -- (BOOL)canBecomeMainWindow { - // Required for NSBorderlessWindowMask windows - for (Map<DisplayServer::WindowID, DisplayServerOSX::WindowData>::Element *E = DS_OSX->windows.front(); E; E = E->next()) { - if (E->get().window_object == self) { - if (E->get().no_focus) { - return NO; - } - } +#endif +#if defined(VULKAN_ENABLED) + if (context_vulkan) { + context_vulkan->window_resize(p_window, p_width, p_height); } - return YES; +#endif } -@end - -/*************************************************************************/ -/* DisplayServerOSX */ -/*************************************************************************/ - bool DisplayServerOSX::has_feature(Feature p_feature) const { switch (p_feature) { case FEATURE_GLOBAL_MENU: @@ -1480,7 +630,6 @@ bool DisplayServerOSX::has_feature(Feature p_feature) const { case FEATURE_CURSOR_SHAPE: case FEATURE_CUSTOM_CURSOR_SHAPE: case FEATURE_NATIVE_DIALOG: - //case FEATURE_CONSOLE_WINDOW: case FEATURE_IME: case FEATURE_WINDOW_TRANSPARENCY: case FEATURE_HIDPI: @@ -1499,57 +648,13 @@ String DisplayServerOSX::get_name() const { return "OSX"; } -const NSMenu *DisplayServerOSX::_get_menu_root(const String &p_menu_root) const { - const NSMenu *menu = nullptr; - if (p_menu_root == "") { - // Main menu.x - menu = [NSApp mainMenu]; - } else if (p_menu_root.to_lower() == "_dock") { - // macOS dock menu. - menu = dock_menu; - } else { - // Submenu. - if (submenu.has(p_menu_root)) { - menu = submenu[p_menu_root]; - } - } - if (menu == apple_menu) { - // Do not allow to change Apple menu. - return nullptr; - } - return menu; -} - -NSMenu *DisplayServerOSX::_get_menu_root(const String &p_menu_root) { - NSMenu *menu = nullptr; - if (p_menu_root == "") { - // Main menu. - menu = [NSApp mainMenu]; - } else if (p_menu_root.to_lower() == "_dock") { - // macOS dock menu. - menu = dock_menu; - } else { - // Submenu. - if (!submenu.has(p_menu_root)) { - NSMenu *n_menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:p_menu_root.utf8().get_data()]]; - submenu[p_menu_root] = n_menu; - } - menu = submenu[p_menu_root]; - } - if (menu == apple_menu) { - // Do not allow to change Apple menu. - return nullptr; - } - return menu; -} - void DisplayServerOSX::global_menu_add_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Variant &p_tag) { _THREAD_SAFE_METHOD_ NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { NSMenuItem *menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:@""]; - GlobalMenuItem *obj = [[[GlobalMenuItem alloc] init] autorelease]; + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; obj->callback = p_callback; obj->meta = p_tag; obj->checkable = false; @@ -1563,7 +668,7 @@ void DisplayServerOSX::global_menu_add_check_item(const String &p_menu_root, con NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { NSMenuItem *menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:@""]; - GlobalMenuItem *obj = [[[GlobalMenuItem alloc] init] autorelease]; + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; obj->callback = p_callback; obj->meta = p_tag; obj->checkable = true; @@ -1619,7 +724,7 @@ bool DisplayServerOSX::global_menu_is_item_checkable(const String &p_menu_root, if (menu) { const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { - GlobalMenuItem *obj = [menu_item representedObject]; + GodotMenuItem *obj = [menu_item representedObject]; if (obj) { return obj->checkable; } @@ -1635,7 +740,7 @@ Callable DisplayServerOSX::global_menu_get_item_callback(const String &p_menu_ro if (menu) { const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { - GlobalMenuItem *obj = [menu_item representedObject]; + GodotMenuItem *obj = [menu_item representedObject]; if (obj) { return obj->callback; } @@ -1651,7 +756,7 @@ Variant DisplayServerOSX::global_menu_get_item_tag(const String &p_menu_root, in if (menu) { const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { - GlobalMenuItem *obj = [menu_item representedObject]; + GodotMenuItem *obj = [menu_item representedObject]; if (obj) { return obj->meta; } @@ -1667,10 +772,8 @@ String DisplayServerOSX::global_menu_get_item_text(const String &p_menu_root, in if (menu) { const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { - char *utfs = strdup([[menu_item title] UTF8String]); String ret; - ret.parse_utf8(utfs); - free(utfs); + ret.parse_utf8([[menu_item title] UTF8String]); return ret; } } @@ -1725,7 +828,7 @@ void DisplayServerOSX::global_menu_set_item_checkable(const String &p_menu_root, } NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { - GlobalMenuItem *obj = [menu_item representedObject]; + GodotMenuItem *obj = [menu_item representedObject]; obj->checkable = p_checkable; } } @@ -1741,7 +844,7 @@ void DisplayServerOSX::global_menu_set_item_callback(const String &p_menu_root, } NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { - GlobalMenuItem *obj = [menu_item representedObject]; + GodotMenuItem *obj = [menu_item representedObject]; obj->callback = p_callback; } } @@ -1757,7 +860,7 @@ void DisplayServerOSX::global_menu_set_item_tag(const String &p_menu_root, int p } NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { - GlobalMenuItem *obj = [menu_item representedObject]; + GodotMenuItem *obj = [menu_item representedObject]; obj->meta = p_tag; } } @@ -1874,7 +977,6 @@ Error DisplayServerOSX::dialog_show(String p_title, String p_description, Vector p_callback.call((const Variant **)&buttonp, 1, ret, ce); } - [window release]; return OK; } @@ -1896,10 +998,8 @@ Error DisplayServerOSX::dialog_input_text(String p_title, String p_description, [window runModal]; - char *utfs = strdup([[input stringValue] UTF8String]); String ret; - ret.parse_utf8(utfs); - free(utfs); + ret.parse_utf8([[input stringValue] UTF8String]); if (!p_callback.is_null()) { Variant text = ret; @@ -1909,7 +1009,6 @@ Error DisplayServerOSX::dialog_input_text(String p_title, String p_description, p_callback.call((const Variant **)&textp, 1, ret, ce); } - [window release]; return OK; } @@ -1920,7 +1019,8 @@ void DisplayServerOSX::mouse_set_mode(MouseMode p_mode) { return; } - WindowData &wd = windows[MAIN_WINDOW_ID]; + WindowID window_id = windows.has(last_focused_window) ? last_focused_window : MAIN_WINDOW_ID; + WindowData &wd = windows[window_id]; if (p_mode == MOUSE_MODE_CAPTURED) { // Apple Docs state that the display parameter is not used. // "This parameter is not used. By default, you may pass kCGDirectMainDisplay." @@ -1963,9 +1063,7 @@ void DisplayServerOSX::mouse_set_mode(MouseMode p_mode) { mouse_mode = p_mode; if (mouse_mode == MOUSE_MODE_VISIBLE || mouse_mode == MOUSE_MODE_CONFINED) { - CursorShape p_shape = cursor_shape; - cursor_shape = DisplayServer::CURSOR_MAX; - cursor_set_shape(p_shape); + cursor_update_shape(); } } @@ -1973,24 +1071,82 @@ DisplayServer::MouseMode DisplayServerOSX::mouse_get_mode() const { return mouse_mode; } +bool DisplayServerOSX::update_mouse_wrap(WindowData &p_wd, NSPoint &r_delta, NSPoint &r_mpos, NSTimeInterval p_timestamp) { + _THREAD_SAFE_METHOD_ + + if (ignore_warp) { + // Discard late events, before warp. + if (p_timestamp < last_warp) { + return true; + } + ignore_warp = false; + return true; + } + + if (mouse_mode == DisplayServer::MOUSE_MODE_CONFINED || mouse_mode == DisplayServer::MOUSE_MODE_CONFINED_HIDDEN) { + // Discard late events. + if (p_timestamp < last_warp) { + return true; + } + + // Warp affects next event delta, subtract previous warp deltas. + List<WarpEvent>::Element *F = warp_events.front(); + while (F) { + if (F->get().timestamp < p_timestamp) { + List<DisplayServerOSX::WarpEvent>::Element *E = F; + r_delta.x -= E->get().delta.x; + r_delta.y -= E->get().delta.y; + F = F->next(); + warp_events.erase(E); + } else { + F = F->next(); + } + } + + // Confine mouse position to the window, and update delta. + NSRect frame = [p_wd.window_object frame]; + NSPoint conf_pos = r_mpos; + conf_pos.x = CLAMP(conf_pos.x + r_delta.x, 0.f, frame.size.width); + conf_pos.y = CLAMP(conf_pos.y - r_delta.y, 0.f, frame.size.height); + r_delta.x = conf_pos.x - r_mpos.x; + r_delta.y = r_mpos.y - conf_pos.y; + r_mpos = conf_pos; + + // Move mouse cursor. + NSRect point_in_window_rect = NSMakeRect(conf_pos.x, conf_pos.y, 0, 0); + conf_pos = [[p_wd.window_view window] convertRectToScreen:point_in_window_rect].origin; + conf_pos.y = CGDisplayBounds(CGMainDisplayID()).size.height - conf_pos.y; + CGWarpMouseCursorPosition(conf_pos); + + // Save warp data. + last_warp = [[NSProcessInfo processInfo] systemUptime]; + + DisplayServerOSX::WarpEvent ev; + ev.timestamp = last_warp; + ev.delta = r_delta; + warp_events.push_back(ev); + } + + return false; +} + void DisplayServerOSX::mouse_warp_to_position(const Point2i &p_to) { _THREAD_SAFE_METHOD_ - if (mouse_mode == MOUSE_MODE_CAPTURED) { - last_mouse_pos = p_to; - } else { - WindowData &wd = windows[MAIN_WINDOW_ID]; + if (mouse_mode != MOUSE_MODE_CAPTURED) { + WindowID window_id = windows.has(last_focused_window) ? last_focused_window : MAIN_WINDOW_ID; + WindowData &wd = windows[window_id]; - //local point in window coords + // Local point in window coords. const NSRect contentRect = [wd.window_view frame]; const float scale = screen_get_max_scale(); NSRect pointInWindowRect = NSMakeRect(p_to.x / scale, contentRect.size.height - (p_to.y / scale - 1), 0, 0); NSPoint pointOnScreen = [[wd.window_view window] convertRectToScreen:pointInWindowRect].origin; - //point in scren coords + // Point in scren coords. CGPoint lMouseWarpPos = { pointOnScreen.x, CGDisplayBounds(CGMainDisplayID()).size.height - pointOnScreen.y }; - //do the warping + // Do the warping. CGEventSourceRef lEventRef = CGEventSourceCreate(kCGEventSourceStateCombinedSessionState); CGEventSourceSetLocalEventsSuppressionInterval(lEventRef, 0.0); CGAssociateMouseAndMouseCursorPosition(false); @@ -2002,10 +1158,6 @@ void DisplayServerOSX::mouse_warp_to_position(const Point2i &p_to) { } Point2i DisplayServerOSX::mouse_get_position() const { - return last_mouse_pos; -} - -Point2i DisplayServerOSX::mouse_get_absolute_position() const { _THREAD_SAFE_METHOD_ const NSPoint mouse_pos = [NSEvent mouseLocation]; @@ -2020,6 +1172,10 @@ Point2i DisplayServerOSX::mouse_get_absolute_position() const { return Vector2i(); } +void DisplayServerOSX::mouse_set_button_state(MouseButton p_state) { + last_button_state = p_state; +} + MouseButton DisplayServerOSX::mouse_get_button_state() const { return last_button_state; } @@ -2051,11 +1207,8 @@ String DisplayServerOSX::clipboard_get() const { NSArray *objectsToPaste = [pasteboard readObjectsForClasses:classArray options:options]; NSString *string = [objectsToPaste objectAtIndex:0]; - char *utfs = strdup([string UTF8String]); String ret; - ret.parse_utf8(utfs); - free(utfs); - + ret.parse_utf8([string UTF8String]); return ret; } @@ -2066,50 +1219,6 @@ int DisplayServerOSX::get_screen_count() const { return [screenArray count]; } -// Returns the native top-left screen coordinate of the smallest rectangle -// that encompasses all screens. Needed in get_screen_position(), -// window_get_position, and window_set_position() -// to convert between OS X native screen coordinates and the ones expected by Godot - -static bool displays_arrangement_dirty = true; -static bool displays_scale_dirty = true; -static void displays_arrangement_changed(CGDirectDisplayID display_id, CGDisplayChangeSummaryFlags flags, void *user_info) { - displays_arrangement_dirty = true; - displays_scale_dirty = true; -} - -Point2i DisplayServerOSX::_get_screens_origin() const { - static Point2i origin; - - if (displays_arrangement_dirty) { - origin = Point2i(); - - for (int i = 0; i < get_screen_count(); i++) { - Point2i position = _get_native_screen_position(i); - if (position.x < origin.x) { - origin.x = position.x; - } - if (position.y > origin.y) { - origin.y = position.y; - } - } - displays_arrangement_dirty = false; - } - - return origin; -} - -Point2i DisplayServerOSX::_get_native_screen_position(int p_screen) const { - NSArray *screenArray = [NSScreen screens]; - if ((NSUInteger)p_screen < [screenArray count]) { - NSRect nsrect = [[screenArray objectAtIndex:p_screen] frame]; - // Return the top-left corner of the screen, for OS X the y starts at the bottom - return Point2i(nsrect.origin.x, nsrect.origin.y + nsrect.size.height) * screen_get_max_scale(); - } - - return Point2i(); -} - Point2i DisplayServerOSX::screen_get_position(int p_screen) const { _THREAD_SAFE_METHOD_ @@ -2119,7 +1228,7 @@ Point2i DisplayServerOSX::screen_get_position(int p_screen) const { Point2i position = _get_native_screen_position(p_screen) - _get_screens_origin(); // OS X native y-coordinate relative to _get_screens_origin() is negative, - // Godot expects a positive value + // Godot expects a positive value. position.y *= -1; return position; } @@ -2133,7 +1242,7 @@ Size2i DisplayServerOSX::screen_get_size(int p_screen) const { NSArray *screenArray = [NSScreen screens]; if ((NSUInteger)p_screen < [screenArray count]) { - // Note: Use frame to get the whole screen size + // Note: Use frame to get the whole screen size. NSRect nsrect = [[screenArray objectAtIndex:p_screen] frame]; return Size2i(nsrect.size.width, nsrect.size.height) * screen_get_max_scale(); } @@ -2186,15 +1295,8 @@ float DisplayServerOSX::screen_get_scale(int p_screen) const { float DisplayServerOSX::screen_get_max_scale() const { _THREAD_SAFE_METHOD_ - static float scale = 1.f; - if (displays_scale_dirty) { - int screen_count = get_screen_count(); - for (int i = 0; i < screen_count; i++) { - scale = fmax(scale, screen_get_scale(i)); - } - displays_scale_dirty = false; - } - return scale; + // Note: Do not update max display scale on screen configuration change, existing editor windows can't be rescaled on the fly. + return display_max_scale; } Rect2i DisplayServerOSX::screen_get_usable_rect(int p_screen) const { @@ -2219,6 +1321,24 @@ Rect2i DisplayServerOSX::screen_get_usable_rect(int p_screen) const { return Rect2i(); } +float DisplayServerOSX::screen_get_refresh_rate(int p_screen) const { + _THREAD_SAFE_METHOD_ + + if (p_screen == SCREEN_OF_MAIN_WINDOW) { + p_screen = window_get_current_screen(); + } + + NSArray *screenArray = [NSScreen screens]; + if ((NSUInteger)p_screen < [screenArray count]) { + NSDictionary *description = [[screenArray objectAtIndex:p_screen] deviceDescription]; + const CGDisplayModeRef displayMode = CGDisplayCopyDisplayMode([[description objectForKey:@"NSScreenNumber"] unsignedIntValue]); + const double displayRefreshRate = CGDisplayModeGetRefreshRate(displayMode); + return (float)displayRefreshRate; + } + ERR_PRINT("An error occured while trying to get the screen refresh rate."); + return SCREEN_REFRESH_RATE_FALLBACK; +} + Vector<DisplayServer::WindowID> DisplayServerOSX::get_window_list() const { _THREAD_SAFE_METHOD_ @@ -2252,56 +1372,6 @@ void DisplayServerOSX::show_window(WindowID p_id) { } } -void DisplayServerOSX::_send_window_event(const WindowData &wd, WindowEvent p_event) { - _THREAD_SAFE_METHOD_ - - if (!wd.event_callback.is_null()) { - Variant event = int(p_event); - Variant *eventp = &event; - Variant ret; - Callable::CallError ce; - wd.event_callback.call((const Variant **)&eventp, 1, ret, ce); - } -} - -DisplayServerOSX::WindowID DisplayServerOSX::_find_window_id(id p_window) { - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - if (E->get().window_object == p_window) { - return E->key(); - } - } - return INVALID_WINDOW_ID; -} - -void DisplayServerOSX::_update_window(WindowData p_wd) { - bool borderless_full = false; - - if (p_wd.borderless) { - NSRect frameRect = [p_wd.window_object frame]; - NSRect screenRect = [[p_wd.window_object screen] frame]; - - // Check if our window covers up the screen - if (frameRect.origin.x <= screenRect.origin.x && frameRect.origin.y <= frameRect.origin.y && - frameRect.size.width >= screenRect.size.width && frameRect.size.height >= screenRect.size.height) { - borderless_full = true; - } - } - - if (borderless_full) { - // If the window covers up the screen set the level to above the main menu and hide on deactivate - [p_wd.window_object setLevel:NSMainMenuWindowLevel + 1]; - [p_wd.window_object setHidesOnDeactivate:YES]; - } else { - // Reset these when our window is not a borderless window that covers up the screen - if (p_wd.on_top && !p_wd.fullscreen) { - [p_wd.window_object setLevel:NSFloatingWindowLevel]; - } else { - [p_wd.window_object setLevel:NSNormalWindowLevel]; - } - [p_wd.window_object setHidesOnDeactivate:NO]; - } -} - void DisplayServerOSX::delete_sub_window(WindowID p_id) { _THREAD_SAFE_METHOD_ @@ -2314,24 +1384,6 @@ void DisplayServerOSX::delete_sub_window(WindowID p_id) { [wd.window_object close]; } -void DisplayServerOSX::window_set_title(const String &p_title, WindowID p_window) { - _THREAD_SAFE_METHOD_ - - ERR_FAIL_COND(!windows.has(p_window)); - WindowData &wd = windows[p_window]; - - [wd.window_object setTitle:[NSString stringWithUTF8String:p_title.utf8().get_data()]]; -} - -void DisplayServerOSX::window_set_mouse_passthrough(const Vector<Vector2> &p_region, WindowID p_window) { - _THREAD_SAFE_METHOD_ - - ERR_FAIL_COND(!windows.has(p_window)); - WindowData &wd = windows[p_window]; - - wd.mpath = p_region; -} - void DisplayServerOSX::window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window) { _THREAD_SAFE_METHOD_ @@ -2370,6 +1422,24 @@ void DisplayServerOSX::window_set_drop_files_callback(const Callable &p_callable wd.drop_files_callback = p_callable; } +void DisplayServerOSX::window_set_title(const String &p_title, WindowID p_window) { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!windows.has(p_window)); + WindowData &wd = windows[p_window]; + + [wd.window_object setTitle:[NSString stringWithUTF8String:p_title.utf8().get_data()]]; +} + +void DisplayServerOSX::window_set_mouse_passthrough(const Vector<Vector2> &p_region, WindowID p_window) { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!windows.has(p_window)); + WindowData &wd = windows[p_window]; + + wd.mpath = p_region; +} + int DisplayServerOSX::window_get_current_screen(WindowID p_window) const { _THREAD_SAFE_METHOD_ ERR_FAIL_COND_V(!windows.has(p_window), -1); @@ -2381,40 +1451,23 @@ int DisplayServerOSX::window_get_current_screen(WindowID p_window) const { void DisplayServerOSX::window_set_current_screen(int p_screen, WindowID p_window) { _THREAD_SAFE_METHOD_ - Point2i wpos = window_get_position(p_window) - screen_get_position(window_get_current_screen(p_window)); - window_set_position(wpos + screen_get_position(p_screen), p_window); -} - -void DisplayServerOSX::window_set_transient(WindowID p_window, WindowID p_parent) { - _THREAD_SAFE_METHOD_ - ERR_FAIL_COND(p_window == p_parent); ERR_FAIL_COND(!windows.has(p_window)); - WindowData &wd_window = windows[p_window]; - - ERR_FAIL_COND(wd_window.transient_parent == p_parent); - - ERR_FAIL_COND_MSG(wd_window.on_top, "Windows with the 'on top' can't become transient."); - if (p_parent == INVALID_WINDOW_ID) { - //remove transient - ERR_FAIL_COND(wd_window.transient_parent == INVALID_WINDOW_ID); - ERR_FAIL_COND(!windows.has(wd_window.transient_parent)); - - WindowData &wd_parent = windows[wd_window.transient_parent]; - - wd_window.transient_parent = INVALID_WINDOW_ID; - wd_parent.transient_children.erase(p_window); + WindowData &wd = windows[p_window]; - [wd_parent.window_object removeChildWindow:wd_window.window_object]; - } else { - ERR_FAIL_COND(!windows.has(p_parent)); - ERR_FAIL_COND_MSG(wd_window.transient_parent != INVALID_WINDOW_ID, "Window already has a transient parent"); - WindowData &wd_parent = windows[p_parent]; + bool was_fullscreen = false; + if (wd.fullscreen) { + // Temporary exit fullscreen mode to move window. + [wd.window_object toggleFullScreen:nil]; + was_fullscreen = true; + } - wd_window.transient_parent = p_parent; - wd_parent.transient_children.insert(p_window); + Point2i wpos = window_get_position(p_window) - screen_get_position(window_get_current_screen(p_window)); + window_set_position(wpos + screen_get_position(p_screen), p_window); - [wd_parent.window_object addChildWindow:wd_window.window_object ordered:NSWindowAbove]; + if (was_fullscreen) { + // Re-enter fullscreen mode. + [wd.window_object toggleFullScreen:nil]; } } @@ -2424,17 +1477,19 @@ Point2i DisplayServerOSX::window_get_position(WindowID p_window) const { ERR_FAIL_COND_V(!windows.has(p_window), Point2i()); const WindowData &wd = windows[p_window]; - NSRect nsrect = [wd.window_object frame]; + // Use content rect position (without titlebar / window border). + const NSRect contentRect = [wd.window_view frame]; + const NSRect nsrect = [wd.window_object convertRectToScreen:contentRect]; Point2i pos; - // Return the position of the top-left corner, for OS X the y starts at the bottom + // Return the position of the top-left corner, for OS X the y starts at the bottom. const float scale = screen_get_max_scale(); pos.x = nsrect.origin.x; pos.y = (nsrect.origin.y + nsrect.size.height); pos *= scale; pos -= _get_screens_origin(); // OS X native y-coordinate relative to _get_screens_origin() is negative, - // Godot expects a positive value + // Godot expects a positive value. pos.y *= -1; return pos; } @@ -2447,15 +1502,57 @@ void DisplayServerOSX::window_set_position(const Point2i &p_position, WindowID p Point2i position = p_position; // OS X native y-coordinate relative to _get_screens_origin() is negative, - // Godot passes a positive value + // Godot passes a positive value. position.y *= -1; position += _get_screens_origin(); position /= screen_get_max_scale(); - [wd.window_object setFrameTopLeftPoint:NSMakePoint(position.x, position.y)]; + // Remove titlebar / window border size. + const NSRect contentRect = [wd.window_view frame]; + const NSRect windowRect = [wd.window_object frame]; + const NSRect nsrect = [wd.window_object convertRectToScreen:contentRect]; + Point2i offset; + offset.x = (nsrect.origin.x - windowRect.origin.x); + offset.y = (nsrect.origin.y + nsrect.size.height); + offset.y -= (windowRect.origin.y + windowRect.size.height); + + [wd.window_object setFrameTopLeftPoint:NSMakePoint(position.x - offset.x, position.y - offset.y)]; - _update_window(wd); - _get_mouse_pos(wd, [wd.window_object mouseLocationOutsideOfEventStream]); + _update_window_style(wd); + update_mouse_pos(wd, [wd.window_object mouseLocationOutsideOfEventStream]); +} + +void DisplayServerOSX::window_set_transient(WindowID p_window, WindowID p_parent) { + _THREAD_SAFE_METHOD_ + ERR_FAIL_COND(p_window == p_parent); + + ERR_FAIL_COND(!windows.has(p_window)); + WindowData &wd_window = windows[p_window]; + + ERR_FAIL_COND(wd_window.transient_parent == p_parent); + + ERR_FAIL_COND_MSG(wd_window.on_top, "Windows with the 'on top' can't become transient."); + if (p_parent == INVALID_WINDOW_ID) { + // Remove transient. + ERR_FAIL_COND(wd_window.transient_parent == INVALID_WINDOW_ID); + ERR_FAIL_COND(!windows.has(wd_window.transient_parent)); + + WindowData &wd_parent = windows[wd_window.transient_parent]; + + wd_window.transient_parent = INVALID_WINDOW_ID; + wd_parent.transient_children.erase(p_window); + + [wd_window.window_object setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; + } else { + ERR_FAIL_COND(!windows.has(p_parent)); + ERR_FAIL_COND_MSG(wd_window.transient_parent != INVALID_WINDOW_ID, "Window already has a transient parent"); + WindowData &wd_parent = windows[p_parent]; + + wd_window.transient_parent = p_parent; + wd_parent.transient_children.insert(p_window); + + [wd_window.window_object setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary]; + } } void DisplayServerOSX::window_set_max_size(const Size2i p_size, WindowID p_window) { @@ -2536,7 +1633,7 @@ void DisplayServerOSX::window_set_size(const Size2i p_size, WindowID p_window) { [wd.window_object setFrame:new_frame display:YES]; - _update_window(wd); + _update_window_style(wd); } Size2i DisplayServerOSX::window_get_size(WindowID p_window) const { @@ -2556,73 +1653,6 @@ Size2i DisplayServerOSX::window_get_real_size(WindowID p_window) const { return Size2i(frame.size.width, frame.size.height) * screen_get_max_scale(); } -bool DisplayServerOSX::window_is_maximize_allowed(WindowID p_window) const { - return true; -} - -void DisplayServerOSX::_set_window_per_pixel_transparency_enabled(bool p_enabled, WindowID p_window) { - ERR_FAIL_COND(!windows.has(p_window)); - WindowData &wd = windows[p_window]; - - if (!OS_OSX::get_singleton()->is_layered_allowed()) { - return; - } - if (wd.layered_window != p_enabled) { - if (p_enabled) { - [wd.window_object setBackgroundColor:[NSColor clearColor]]; - [wd.window_object setOpaque:NO]; - [wd.window_object setHasShadow:NO]; - CALayer *layer = [wd.window_view layer]; - if (layer) { - [layer setOpaque:NO]; - } -#if defined(VULKAN_ENABLED) - if (rendering_driver == "vulkan") { - //TODO - implement transparency for Vulkan - } -#endif -#if defined(GLES3_ENABLED) - if (rendering_driver == "opengl3") { - //TODO - reimplement OpenGLES - } -#endif - wd.layered_window = true; - } else { - [wd.window_object setBackgroundColor:[NSColor colorWithCalibratedWhite:1 alpha:1]]; - [wd.window_object setOpaque:YES]; - [wd.window_object setHasShadow:YES]; - CALayer *layer = [wd.window_view layer]; - if (layer) { - [layer setOpaque:YES]; - } -#if defined(VULKAN_ENABLED) - if (rendering_driver == "vulkan") { - //TODO - implement transparency for Vulkan - } -#endif -#if defined(GLES3_ENABLED) - if (rendering_driver == "opengl3") { - //TODO - reimplement OpenGLES - } -#endif - wd.layered_window = false; - } -#if defined(GLES3_ENABLED) - if (rendering_driver == "opengl3") { - //TODO - reimplement OpenGLES - } -#endif -#if defined(VULKAN_ENABLED) - if (rendering_driver == "vulkan") { - //TODO - implement transparency for Vulkan - } -#endif - NSRect frameRect = [wd.window_object frame]; - [wd.window_object setFrame:NSMakeRect(frameRect.origin.x, frameRect.origin.y, frameRect.size.width + 1, frameRect.size.height) display:YES]; - [wd.window_object setFrame:frameRect display:YES]; - } -} - void DisplayServerOSX::window_set_mode(WindowMode p_mode, WindowID p_window) { _THREAD_SAFE_METHOD_ @@ -2631,22 +1661,21 @@ void DisplayServerOSX::window_set_mode(WindowMode p_mode, WindowID p_window) { WindowMode old_mode = window_get_mode(p_window); if (old_mode == p_mode) { - return; // do nothing + return; // Do nothing. } switch (old_mode) { case WINDOW_MODE_WINDOWED: { - //do nothing + // Do nothing. } break; case WINDOW_MODE_MINIMIZED: { [wd.window_object deminiaturize:nil]; } break; + case WINDOW_MODE_EXCLUSIVE_FULLSCREEN: case WINDOW_MODE_FULLSCREEN: { [wd.window_object setLevel:NSNormalWindowLevel]; - if (wd.layered_window) { - _set_window_per_pixel_transparency_enabled(true, p_window); - } - if (wd.resize_disabled) { //restore resize disabled + _set_window_per_pixel_transparency_enabled(true, p_window); + if (wd.resize_disabled) { // Restore resize disabled. [wd.window_object setStyleMask:[wd.window_object styleMask] & ~NSWindowStyleMaskResizable]; } if (wd.min_size != Size2i()) { @@ -2669,16 +1698,17 @@ void DisplayServerOSX::window_set_mode(WindowMode p_mode, WindowID p_window) { switch (p_mode) { case WINDOW_MODE_WINDOWED: { - //do nothing + // Do nothing. } break; case WINDOW_MODE_MINIMIZED: { [wd.window_object performMiniaturize:nil]; } break; + case WINDOW_MODE_EXCLUSIVE_FULLSCREEN: case WINDOW_MODE_FULLSCREEN: { - if (wd.layered_window) - _set_window_per_pixel_transparency_enabled(false, p_window); - if (wd.resize_disabled) //fullscreen window should be resizable to work + _set_window_per_pixel_transparency_enabled(false, p_window); + if (wd.resize_disabled) { // Fullscreen window should be resizable to work. [wd.window_object setStyleMask:[wd.window_object styleMask] | NSWindowStyleMaskResizable]; + } [wd.window_object setContentMinSize:NSMakeSize(0, 0)]; [wd.window_object setContentMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)]; [wd.window_object toggleFullScreen:nil]; @@ -2698,7 +1728,7 @@ DisplayServer::WindowMode DisplayServerOSX::window_get_mode(WindowID p_window) c ERR_FAIL_COND_V(!windows.has(p_window), WINDOW_MODE_WINDOWED); const WindowData &wd = windows[p_window]; - if (wd.fullscreen) { //if fullscreen, it's not in another mode + if (wd.fullscreen) { // If fullscreen, it's not in another mode. return WINDOW_MODE_FULLSCREEN; } if ([wd.window_object isZoomed] && !wd.resize_disabled) { @@ -2710,10 +1740,14 @@ DisplayServer::WindowMode DisplayServerOSX::window_get_mode(WindowID p_window) c } } - // all other discarded, return windowed. + // All other discarded, return windowed. return WINDOW_MODE_WINDOWED; } +bool DisplayServerOSX::window_is_maximize_allowed(WindowID p_window) const { + return true; +} + void DisplayServerOSX::window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window) { _THREAD_SAFE_METHOD_ @@ -2723,7 +1757,7 @@ void DisplayServerOSX::window_set_flag(WindowFlags p_flag, bool p_enabled, Windo switch (p_flag) { case WINDOW_FLAG_RESIZE_DISABLED: { wd.resize_disabled = p_enabled; - if (wd.fullscreen) { //fullscreen window should be resizable, style will be applied on exiting fs + if (wd.fullscreen) { // Fullscreen window should be resizable, style will be applied on exiting fullscreen. return; } if (p_enabled) { @@ -2733,7 +1767,7 @@ void DisplayServerOSX::window_set_flag(WindowFlags p_flag, bool p_enabled, Windo } } break; case WINDOW_FLAG_BORDERLESS: { - // OrderOut prevents a lose focus bug with the window + // OrderOut prevents a lose focus bug with the window. if ([wd.window_object isVisible]) { [wd.window_object orderOut:nil]; } @@ -2741,15 +1775,14 @@ void DisplayServerOSX::window_set_flag(WindowFlags p_flag, bool p_enabled, Windo if (p_enabled) { [wd.window_object setStyleMask:NSWindowStyleMaskBorderless]; } else { - if (wd.layered_window) - _set_window_per_pixel_transparency_enabled(false, p_window); + _set_window_per_pixel_transparency_enabled(false, p_window); [wd.window_object setStyleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | (wd.resize_disabled ? 0 : NSWindowStyleMaskResizable)]; - // Force update of the window styles + // Force update of the window styles. NSRect frameRect = [wd.window_object frame]; [wd.window_object setFrame:NSMakeRect(frameRect.origin.x, frameRect.origin.y, frameRect.size.width + 1, frameRect.size.height) display:NO]; [wd.window_object setFrame:frameRect display:NO]; } - _update_window(wd); + _update_window_style(wd); if ([wd.window_object isVisible]) { if (wd.no_focus) { [wd.window_object orderFront:nil]; @@ -2770,9 +1803,8 @@ void DisplayServerOSX::window_set_flag(WindowFlags p_flag, bool p_enabled, Windo } } break; case WINDOW_FLAG_TRANSPARENT: { - wd.layered_window = p_enabled; if (p_enabled) { - [wd.window_object setStyleMask:NSWindowStyleMaskBorderless]; // force borderless + [wd.window_object setStyleMask:NSWindowStyleMaskBorderless]; // Force borderless. } else if (!wd.borderless) { [wd.window_object setStyleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | (wd.resize_disabled ? 0 : NSWindowStyleMaskResizable)]; } @@ -2875,28 +1907,103 @@ void DisplayServerOSX::window_set_ime_position(const Point2i &p_pos, WindowID p_ wd.im_position = p_pos; } -bool DisplayServerOSX::get_swap_cancel_ok() { - return false; +DisplayServer::WindowID DisplayServerOSX::get_window_at_screen_position(const Point2i &p_position) const { + Point2i position = p_position; + position.y *= -1; + position += _get_screens_origin(); + position /= screen_get_max_scale(); + + NSInteger wnum = [NSWindow windowNumberAtPoint:NSMakePoint(position.x, position.y) belowWindowWithWindowNumber:0 /*topmost*/]; + for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { + if ([E->get().window_object windowNumber] == wnum) { + return E->key(); + } + } + return INVALID_WINDOW_ID; } -void DisplayServerOSX::cursor_set_shape(CursorShape p_shape) { +int64_t DisplayServerOSX::window_get_native_handle(HandleType p_handle_type, WindowID p_window) const { + ERR_FAIL_COND_V(!windows.has(p_window), 0); + switch (p_handle_type) { + case DISPLAY_HANDLE: { + return 0; // Not supported. + } + case WINDOW_HANDLE: { + return (int64_t)windows[p_window].window_object; + } + case WINDOW_VIEW: { + return (int64_t)windows[p_window].window_view; + } + default: { + return 0; + } + } +} + +void DisplayServerOSX::window_attach_instance_id(ObjectID p_instance, WindowID p_window) { _THREAD_SAFE_METHOD_ - ERR_FAIL_INDEX(p_shape, CURSOR_MAX); + ERR_FAIL_COND(!windows.has(p_window)); + windows[p_window].instance_id = p_instance; +} - if (cursor_shape == p_shape) { - return; +ObjectID DisplayServerOSX::window_get_attached_instance_id(WindowID p_window) const { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND_V(!windows.has(p_window), ObjectID()); + return windows[p_window].instance_id; +} + +void DisplayServerOSX::gl_window_make_current(DisplayServer::WindowID p_window_id) { +#if defined(GLES3_ENABLED) + gl_manager->window_make_current(p_window_id); +#endif +} + +void DisplayServerOSX::window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window) { + _THREAD_SAFE_METHOD_ +#if defined(GLES3_ENABLED) + if (gl_manager) { + gl_manager->set_use_vsync(p_vsync_mode); } +#endif +#if defined(VULKAN_ENABLED) + if (context_vulkan) { + context_vulkan->set_vsync_mode(p_window, p_vsync_mode); + } +#endif +} - if (mouse_mode != MOUSE_MODE_VISIBLE && mouse_mode != MOUSE_MODE_CONFINED) { - cursor_shape = p_shape; - return; +DisplayServer::VSyncMode DisplayServerOSX::window_get_vsync_mode(WindowID p_window) const { + _THREAD_SAFE_METHOD_ +#if defined(GLES3_ENABLED) + if (gl_manager) { + return (gl_manager->is_using_vsync() ? DisplayServer::VSyncMode::VSYNC_ENABLED : DisplayServer::VSyncMode::VSYNC_DISABLED); } +#endif +#if defined(VULKAN_ENABLED) + if (context_vulkan) { + return context_vulkan->get_vsync_mode(p_window); + } +#endif + return DisplayServer::VSYNC_ENABLED; +} + +Point2i DisplayServerOSX::ime_get_selection() const { + return im_selection; +} + +String DisplayServerOSX::ime_get_text() const { + return im_text; +} - if (cursors[p_shape] != nullptr) { - [cursors[p_shape] set]; +void DisplayServerOSX::cursor_update_shape() { + _THREAD_SAFE_METHOD_ + + if (cursors[cursor_shape] != nullptr) { + [cursors[cursor_shape] set]; } else { - switch (p_shape) { + switch (cursor_shape) { case CURSOR_ARROW: [[NSCursor arrowCursor] set]; break; @@ -2925,16 +2032,16 @@ void DisplayServerOSX::cursor_set_shape(CursorShape p_shape) { [[NSCursor operationNotAllowedCursor] set]; break; case CURSOR_VSIZE: - [_cursorFromSelector(@selector(_windowResizeNorthSouthCursor), @selector(resizeUpDownCursor)) set]; + [_cursor_from_selector(@selector(_windowResizeNorthSouthCursor), @selector(resizeUpDownCursor)) set]; break; case CURSOR_HSIZE: - [_cursorFromSelector(@selector(_windowResizeEastWestCursor), @selector(resizeLeftRightCursor)) set]; + [_cursor_from_selector(@selector(_windowResizeEastWestCursor), @selector(resizeLeftRightCursor)) set]; break; case CURSOR_BDIAGSIZE: - [_cursorFromSelector(@selector(_windowResizeNorthEastSouthWestCursor)) set]; + [_cursor_from_selector(@selector(_windowResizeNorthEastSouthWestCursor)) set]; break; case CURSOR_FDIAGSIZE: - [_cursorFromSelector(@selector(_windowResizeNorthWestSouthEastCursor)) set]; + [_cursor_from_selector(@selector(_windowResizeNorthWestSouthEastCursor)) set]; break; case CURSOR_MOVE: [[NSCursor arrowCursor] set]; @@ -2946,14 +2053,30 @@ void DisplayServerOSX::cursor_set_shape(CursorShape p_shape) { [[NSCursor resizeLeftRightCursor] set]; break; case CURSOR_HELP: - [_cursorFromSelector(@selector(_helpCursor)) set]; + [_cursor_from_selector(@selector(_helpCursor)) set]; break; default: { } } } +} + +void DisplayServerOSX::cursor_set_shape(CursorShape p_shape) { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_INDEX(p_shape, CURSOR_MAX); + + if (cursor_shape == p_shape) { + return; + } cursor_shape = p_shape; + + if (mouse_mode != MOUSE_MODE_VISIBLE && mouse_mode != MOUSE_MODE_CONFINED) { + return; + } + + cursor_update_shape(); } DisplayServerOSX::CursorShape DisplayServerOSX::cursor_get_shape() const { @@ -3048,7 +2171,6 @@ void DisplayServerOSX::cursor_set_custom_image(const RES &p_cursor, CursorShape NSCursor *cursor = [[NSCursor alloc] initWithImage:nsimage hotSpot:NSMakePoint(p_hotspot.x, p_hotspot.y)]; - [cursors[p_shape] release]; cursors[p_shape] = cursor; Vector<Variant> params; @@ -3061,94 +2183,32 @@ void DisplayServerOSX::cursor_set_custom_image(const RES &p_cursor, CursorShape [cursor set]; } } - - [imgrep release]; - [nsimage release]; } else { - // Reset to default system cursor + // Reset to default system cursor. if (cursors[p_shape] != nullptr) { - [cursors[p_shape] release]; cursors[p_shape] = nullptr; } - CursorShape c = cursor_shape; - cursor_shape = CURSOR_MAX; - cursor_set_shape(c); + cursor_update_shape(); cursors_cache.erase(p_shape); } } -struct LayoutInfo { - String name; - String code; -}; - -static Vector<LayoutInfo> kbd_layouts; -static int current_layout = 0; -static bool keyboard_layout_dirty = true; -static void keyboard_layout_changed(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef user_info) { - kbd_layouts.clear(); - current_layout = 0; - keyboard_layout_dirty = true; -} - -void _update_keyboard_layouts() { - @autoreleasepool { - TISInputSourceRef cur_source = TISCopyCurrentKeyboardInputSource(); - NSString *cur_name = (NSString *)TISGetInputSourceProperty(cur_source, kTISPropertyLocalizedName); - CFRelease(cur_source); - - // Enum IME layouts - NSDictionary *filter_ime = @{ (NSString *)kTISPropertyInputSourceType : (NSString *)kTISTypeKeyboardInputMode }; - NSArray *list_ime = (NSArray *)TISCreateInputSourceList((CFDictionaryRef)filter_ime, false); - for (NSUInteger i = 0; i < [list_ime count]; i++) { - LayoutInfo ly; - NSString *name = (NSString *)TISGetInputSourceProperty((TISInputSourceRef)[list_ime objectAtIndex:i], kTISPropertyLocalizedName); - ly.name.parse_utf8([name UTF8String]); - - NSArray *langs = (NSArray *)TISGetInputSourceProperty((TISInputSourceRef)[list_ime objectAtIndex:i], kTISPropertyInputSourceLanguages); - ly.code.parse_utf8([(NSString *)[langs objectAtIndex:0] UTF8String]); - kbd_layouts.push_back(ly); - - if ([name isEqualToString:cur_name]) { - current_layout = kbd_layouts.size() - 1; - } - } - [list_ime release]; - - // Enum plain keyboard layouts - NSDictionary *filter_kbd = @{ (NSString *)kTISPropertyInputSourceType : (NSString *)kTISTypeKeyboardLayout }; - NSArray *list_kbd = (NSArray *)TISCreateInputSourceList((CFDictionaryRef)filter_kbd, false); - for (NSUInteger i = 0; i < [list_kbd count]; i++) { - LayoutInfo ly; - NSString *name = (NSString *)TISGetInputSourceProperty((TISInputSourceRef)[list_kbd objectAtIndex:i], kTISPropertyLocalizedName); - ly.name.parse_utf8([name UTF8String]); - - NSArray *langs = (NSArray *)TISGetInputSourceProperty((TISInputSourceRef)[list_kbd objectAtIndex:i], kTISPropertyInputSourceLanguages); - ly.code.parse_utf8([(NSString *)[langs objectAtIndex:0] UTF8String]); - kbd_layouts.push_back(ly); - - if ([name isEqualToString:cur_name]) { - current_layout = kbd_layouts.size() - 1; - } - } - [list_kbd release]; - } - - keyboard_layout_dirty = false; +bool DisplayServerOSX::get_swap_cancel_ok() { + return false; } int DisplayServerOSX::keyboard_get_layout_count() const { if (keyboard_layout_dirty) { - _update_keyboard_layouts(); + const_cast<DisplayServerOSX *>(this)->_update_keyboard_layouts(); } return kbd_layouts.size(); } void DisplayServerOSX::keyboard_set_current_layout(int p_index) { if (keyboard_layout_dirty) { - _update_keyboard_layouts(); + const_cast<DisplayServerOSX *>(this)->_update_keyboard_layouts(); } ERR_FAIL_INDEX(p_index, kbd_layouts.size()); @@ -3156,31 +2216,29 @@ void DisplayServerOSX::keyboard_set_current_layout(int p_index) { NSString *cur_name = [NSString stringWithUTF8String:kbd_layouts[p_index].name.utf8().get_data()]; NSDictionary *filter_kbd = @{ (NSString *)kTISPropertyInputSourceType : (NSString *)kTISTypeKeyboardLayout }; - NSArray *list_kbd = (NSArray *)TISCreateInputSourceList((CFDictionaryRef)filter_kbd, false); + NSArray *list_kbd = (__bridge NSArray *)TISCreateInputSourceList((__bridge CFDictionaryRef)filter_kbd, false); for (NSUInteger i = 0; i < [list_kbd count]; i++) { - NSString *name = (NSString *)TISGetInputSourceProperty((TISInputSourceRef)[list_kbd objectAtIndex:i], kTISPropertyLocalizedName); + NSString *name = (__bridge NSString *)TISGetInputSourceProperty((__bridge TISInputSourceRef)[list_kbd objectAtIndex:i], kTISPropertyLocalizedName); if ([name isEqualToString:cur_name]) { - TISSelectInputSource((TISInputSourceRef)[list_kbd objectAtIndex:i]); + TISSelectInputSource((__bridge TISInputSourceRef)[list_kbd objectAtIndex:i]); break; } } - [list_kbd release]; NSDictionary *filter_ime = @{ (NSString *)kTISPropertyInputSourceType : (NSString *)kTISTypeKeyboardInputMode }; - NSArray *list_ime = (NSArray *)TISCreateInputSourceList((CFDictionaryRef)filter_ime, false); + NSArray *list_ime = (__bridge NSArray *)TISCreateInputSourceList((__bridge CFDictionaryRef)filter_ime, false); for (NSUInteger i = 0; i < [list_ime count]; i++) { - NSString *name = (NSString *)TISGetInputSourceProperty((TISInputSourceRef)[list_ime objectAtIndex:i], kTISPropertyLocalizedName); + NSString *name = (__bridge NSString *)TISGetInputSourceProperty((__bridge TISInputSourceRef)[list_ime objectAtIndex:i], kTISPropertyLocalizedName); if ([name isEqualToString:cur_name]) { - TISSelectInputSource((TISInputSourceRef)[list_ime objectAtIndex:i]); + TISSelectInputSource((__bridge TISInputSourceRef)[list_ime objectAtIndex:i]); break; } } - [list_ime release]; } int DisplayServerOSX::keyboard_get_current_layout() const { if (keyboard_layout_dirty) { - _update_keyboard_layouts(); + const_cast<DisplayServerOSX *>(this)->_update_keyboard_layouts(); } return current_layout; @@ -3188,7 +2246,7 @@ int DisplayServerOSX::keyboard_get_current_layout() const { String DisplayServerOSX::keyboard_get_layout_language(int p_index) const { if (keyboard_layout_dirty) { - _update_keyboard_layouts(); + const_cast<DisplayServerOSX *>(this)->_update_keyboard_layouts(); } ERR_FAIL_INDEX_V(p_index, kbd_layouts.size(), ""); @@ -3197,7 +2255,7 @@ String DisplayServerOSX::keyboard_get_layout_language(int p_index) const { String DisplayServerOSX::keyboard_get_layout_name(int p_index) const { if (keyboard_layout_dirty) { - _update_keyboard_layouts(); + const_cast<DisplayServerOSX *>(this)->_update_keyboard_layouts(); } ERR_FAIL_INDEX_V(p_index, kbd_layouts.size(), ""); @@ -3211,124 +2269,8 @@ Key DisplayServerOSX::keyboard_get_keycode_from_physical(Key p_keycode) const { Key modifiers = p_keycode & KeyModifierMask::MODIFIER_MASK; Key keycode_no_mod = p_keycode & KeyModifierMask::CODE_MASK; - unsigned int osx_keycode = unmapKey((Key)keycode_no_mod); - return (Key)(remapKey(osx_keycode, 0) | modifiers); -} - -void DisplayServerOSX::_push_input(const Ref<InputEvent> &p_event) { - Ref<InputEvent> ev = p_event; - Input::get_singleton()->parse_input_event(ev); -} - -void DisplayServerOSX::_release_pressed_events() { - _THREAD_SAFE_METHOD_ - if (Input::get_singleton()) { - Input::get_singleton()->release_pressed_events(); - } -} - -NSMenu *DisplayServerOSX::_get_dock_menu() const { - return dock_menu; -} - -void DisplayServerOSX::_menu_callback(id p_sender) { - if (![p_sender representedObject]) { - return; - } - - GlobalMenuItem *value = [p_sender representedObject]; - - if (value) { - if (value->checkable) { - if ([p_sender state] == NSControlStateValueOff) { - [p_sender setState:NSControlStateValueOn]; - } else { - [p_sender setState:NSControlStateValueOff]; - } - } - - if (value->callback != Callable()) { - Variant tag = value->meta; - Variant *tagp = &tag; - Variant ret; - Callable::CallError ce; - value->callback.call((const Variant **)&tagp, 1, ret, ce); - } - } -} - -void DisplayServerOSX::_send_event(NSEvent *p_event) { - // special case handling of command-period, which is traditionally a special - // shortcut in macOS and doesn't arrive at our regular keyDown handler. - if ([p_event type] == NSEventTypeKeyDown) { - if (([p_event modifierFlags] & NSEventModifierFlagCommand) && [p_event keyCode] == 0x2f) { - Ref<InputEventKey> k; - k.instantiate(); - - _get_key_modifier_state([p_event modifierFlags], k); - k->set_window_id(DisplayServerOSX::INVALID_WINDOW_ID); - k->set_pressed(true); - k->set_keycode(Key::PERIOD); - k->set_physical_keycode(Key::PERIOD); - k->set_echo([p_event isARepeat]); - - Input::get_singleton()->parse_input_event(k); - } - } -} - -void DisplayServerOSX::_process_key_events() { - Ref<InputEventKey> k; - for (int i = 0; i < key_event_pos; i++) { - const KeyEvent &ke = key_event_buffer[i]; - if (ke.raw) { - // Non IME input - no composite characters, pass events as is - k.instantiate(); - - k->set_window_id(ke.window_id); - _get_key_modifier_state(ke.osx_state, k); - k->set_pressed(ke.pressed); - k->set_echo(ke.echo); - k->set_keycode(ke.keycode); - k->set_physical_keycode((Key)ke.physical_keycode); - k->set_unicode(ke.unicode); - - _push_input(k); - } else { - // IME input - if ((i == 0 && ke.keycode == Key::NONE) || (i > 0 && key_event_buffer[i - 1].keycode == Key::NONE)) { - k.instantiate(); - - k->set_window_id(ke.window_id); - _get_key_modifier_state(ke.osx_state, k); - k->set_pressed(ke.pressed); - k->set_echo(ke.echo); - k->set_keycode(Key::NONE); - k->set_physical_keycode(Key::NONE); - k->set_unicode(ke.unicode); - - _push_input(k); - } - if (ke.keycode != Key::NONE) { - k.instantiate(); - - k->set_window_id(ke.window_id); - _get_key_modifier_state(ke.osx_state, k); - k->set_pressed(ke.pressed); - k->set_echo(ke.echo); - k->set_keycode(ke.keycode); - k->set_physical_keycode((Key)ke.physical_keycode); - - if (i + 1 < key_event_pos && key_event_buffer[i + 1].keycode == Key::NONE) { - k->set_unicode(key_event_buffer[i + 1].unicode); - } - - _push_input(k); - } - } - } - - key_event_pos = 0; + unsigned int osx_keycode = KeyMappingOSX::unmap_key((Key)keycode_no_mod); + return (Key)(KeyMappingOSX::remap_key(osx_keycode, 0) | modifiers); } void DisplayServerOSX::process_events() { @@ -3356,8 +2298,8 @@ void DisplayServerOSX::process_events() { for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { WindowData &wd = E->get(); if (wd.mpath.size() > 0) { - const Vector2 mpos = _get_mouse_pos(wd, [wd.window_object mouseLocationOutsideOfEventStream]); - if (Geometry2D::is_point_in_polygon(mpos, wd.mpath)) { + update_mouse_pos(wd, [wd.window_object mouseLocationOutsideOfEventStream]); + if (Geometry2D::is_point_in_polygon(wd.mouse_pos, wd.mpath)) { if ([wd.window_object ignoresMouseEvents]) { [wd.window_object setIgnoresMouseEvents:NO]; } @@ -3372,9 +2314,6 @@ void DisplayServerOSX::process_events() { } } } - - [autoreleasePool drain]; - autoreleasePool = [[NSAutoreleasePool alloc] init]; } void DisplayServerOSX::force_process_and_drop_events() { @@ -3385,6 +2324,18 @@ void DisplayServerOSX::force_process_and_drop_events() { drop_events = false; } +void DisplayServerOSX::release_rendering_thread() { +} + +void DisplayServerOSX::make_rendering_thread() { +} + +void DisplayServerOSX::swap_buffers() { +#if defined(GLES3_ENABLED) + gl_manager->swap_buffers(); +#endif +} + void DisplayServerOSX::set_native_icon(const String &p_filename) { _THREAD_SAFE_METHOD_ @@ -3397,10 +2348,10 @@ void DisplayServerOSX::set_native_icon(const String &p_filename) { f->get_buffer((uint8_t *)&data.write[0], len); memdelete(f); - NSData *icon_data = [[[NSData alloc] initWithBytes:&data.write[0] length:len] autorelease]; + NSData *icon_data = [[NSData alloc] initWithBytes:&data.write[0] length:len]; ERR_FAIL_COND_MSG(!icon_data, "Error reading icon data."); - NSImage *icon = [[[NSImage alloc] initWithData:icon_data] autorelease]; + NSImage *icon = [[NSImage alloc] initWithData:icon_data]; ERR_FAIL_COND_MSG(!icon, "Error loading icon."); [NSApp setApplicationIconImage:icon]; @@ -3443,94 +2394,6 @@ void DisplayServerOSX::set_icon(const Ref<Image> &p_icon) { [nsimg addRepresentation:imgrep]; [NSApp setApplicationIconImage:nsimg]; - - [imgrep release]; - [nsimg release]; -} - -void DisplayServerOSX::window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window) { - _THREAD_SAFE_METHOD_ -#if defined(GLES3_ENABLED) - if (rendering_driver == "opengl3") { - gl_manager->swap_buffers(); - } -#endif -#if defined(VULKAN_ENABLED) - if (rendering_driver == "vulkan") { - context_vulkan->set_vsync_mode(p_window, p_vsync_mode); - } -#endif -} - -DisplayServer::VSyncMode DisplayServerOSX::window_get_vsync_mode(WindowID p_window) const { - _THREAD_SAFE_METHOD_ -#if defined(GLES3_ENABLED) - if (rendering_driver == "opengl3") { - return (gl_manager->is_using_vsync() ? DisplayServer::VSyncMode::VSYNC_ENABLED : DisplayServer::VSyncMode::VSYNC_DISABLED); - } -#endif -#if defined(VULKAN_ENABLED) - if (rendering_driver == "vulkan") { - return context_vulkan->get_vsync_mode(p_window); - } -#endif - return DisplayServer::VSYNC_ENABLED; -} - -Vector<String> DisplayServerOSX::get_rendering_drivers_func() { - Vector<String> drivers; - -#if defined(VULKAN_ENABLED) - drivers.push_back("vulkan"); -#endif -#if defined(GLES3_ENABLED) - drivers.push_back("opengl3"); -#endif - - return drivers; -} - -void DisplayServerOSX::gl_window_make_current(DisplayServer::WindowID p_window_id) { -#if defined(GLES3_ENABLED) - gl_manager->window_make_current(p_window_id); -#endif -} - -Point2i DisplayServerOSX::ime_get_selection() const { - return im_selection; -} - -String DisplayServerOSX::ime_get_text() const { - return im_text; -} - -DisplayServer::WindowID DisplayServerOSX::get_window_at_screen_position(const Point2i &p_position) const { - Point2i position = p_position; - position.y *= -1; - position += _get_screens_origin(); - position /= screen_get_max_scale(); - - NSInteger wnum = [NSWindow windowNumberAtPoint:NSMakePoint(position.x, position.y) belowWindowWithWindowNumber:0 /*topmost*/]; - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - if ([E->get().window_object windowNumber] == wnum) { - return E->key(); - } - } - return INVALID_WINDOW_ID; -} - -void DisplayServerOSX::window_attach_instance_id(ObjectID p_instance, WindowID p_window) { - _THREAD_SAFE_METHOD_ - - ERR_FAIL_COND(!windows.has(p_window)); - windows[p_window].instance_id = p_instance; -} - -ObjectID DisplayServerOSX::window_get_attached_instance_id(WindowID p_window) const { - _THREAD_SAFE_METHOD_ - - ERR_FAIL_COND_V(!windows.has(p_window), ObjectID()); - return windows[p_window].instance_id; } DisplayServer *DisplayServerOSX::create_func(const String &p_rendering_driver, WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { @@ -3541,186 +2404,48 @@ DisplayServer *DisplayServerOSX::create_func(const String &p_rendering_driver, W return ds; } -DisplayServerOSX::WindowID DisplayServerOSX::_create_window(WindowMode p_mode, VSyncMode p_vsync_mode, const Rect2i &p_rect) { - WindowID id; - const float scale = screen_get_max_scale(); - { - WindowData wd; - - wd.window_delegate = [[GodotWindowDelegate alloc] init]; - ERR_FAIL_COND_V_MSG(wd.window_delegate == nil, INVALID_WINDOW_ID, "Can't create a window delegate"); - [wd.window_delegate setWindowID:window_id_counter]; - - Point2i position = p_rect.position; - // OS X native y-coordinate relative to _get_screens_origin() is negative, - // Godot passes a positive value - position.y *= -1; - position += _get_screens_origin(); - - // initWithContentRect uses bottom-left corner of the window’s frame as origin. - wd.window_object = [[GodotWindow alloc] - initWithContentRect:NSMakeRect(position.x / scale, (position.y - p_rect.size.height) / scale, p_rect.size.width / scale, p_rect.size.height / scale) - styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable - backing:NSBackingStoreBuffered - defer:NO]; - ERR_FAIL_COND_V_MSG(wd.window_object == nil, INVALID_WINDOW_ID, "Can't create a window"); - - wd.window_view = [[GodotContentView alloc] init]; - ERR_FAIL_COND_V_MSG(wd.window_view == nil, INVALID_WINDOW_ID, "Can't create a window view"); - [wd.window_view setWindowID:window_id_counter]; - [wd.window_view setWantsLayer:TRUE]; - - [wd.window_object setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; - [wd.window_object setContentView:wd.window_view]; - [wd.window_object setDelegate:wd.window_delegate]; - [wd.window_object setAcceptsMouseMovedEvents:YES]; - [wd.window_object setRestorable:NO]; - - if ([wd.window_object respondsToSelector:@selector(setTabbingMode:)]) { - [wd.window_object setTabbingMode:NSWindowTabbingModeDisallowed]; - } - - CALayer *layer = [wd.window_view layer]; - if (layer) { - layer.contentsScale = scale; - } - -#if defined(VULKAN_ENABLED) - if (rendering_driver == "vulkan") { - if (context_vulkan) { - Error err = context_vulkan->window_create(window_id_counter, p_vsync_mode, wd.window_view, p_rect.size.width, p_rect.size.height); - ERR_FAIL_COND_V_MSG(err != OK, INVALID_WINDOW_ID, "Can't create a Vulkan context"); - } - } -#endif -#if defined(GLES3_ENABLED) - if (rendering_driver == "opengl3") { - if (gl_manager) { - Error err = gl_manager->window_create(window_id_counter, wd.window_view, p_rect.size.width, p_rect.size.height); - ERR_FAIL_COND_V_MSG(err != OK, INVALID_WINDOW_ID, "Can't create an OpenGL context"); - } - } -#endif - id = window_id_counter++; - windows[id] = wd; - } - - WindowData &wd = windows[id]; - window_set_mode(p_mode, id); - - const NSRect contentRect = [wd.window_view frame]; - wd.size.width = contentRect.size.width * scale; - wd.size.height = contentRect.size.height * scale; - - CALayer *layer = [wd.window_view layer]; - if (layer) { - layer.contentsScale = scale; - } +Vector<String> DisplayServerOSX::get_rendering_drivers_func() { + Vector<String> drivers; -#if defined(GLES3_ENABLED) - if (rendering_driver == "opengl3") { - gl_manager->window_resize(id, wd.size.width, wd.size.height); - } -#endif #if defined(VULKAN_ENABLED) - if (rendering_driver == "vulkan") { - context_vulkan->window_resize(id, wd.size.width, wd.size.height); - } + drivers.push_back("vulkan"); #endif - - return id; -} - -void DisplayServerOSX::_dispatch_input_events(const Ref<InputEvent> &p_event) { - ((DisplayServerOSX *)(get_singleton()))->_dispatch_input_event(p_event); -} - -void DisplayServerOSX::_dispatch_input_event(const Ref<InputEvent> &p_event) { - _THREAD_SAFE_METHOD_ - if (!in_dispatch_input_event) { - in_dispatch_input_event = true; - - Variant ev = p_event; - Variant *evp = &ev; - Variant ret; - Callable::CallError ce; - - Ref<InputEventFromWindow> event_from_window = p_event; - if (event_from_window.is_valid() && event_from_window->get_window_id() != INVALID_WINDOW_ID) { - //send to a window - if (windows.has(event_from_window->get_window_id())) { - Callable callable = windows[event_from_window->get_window_id()].input_event_callback; - if (callable.is_null()) { - return; - } - callable.call((const Variant **)&evp, 1, ret, ce); - } - } else { - //send to all windows - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - Callable callable = E->get().input_event_callback; - if (callable.is_null()) { - continue; - } - callable.call((const Variant **)&evp, 1, ret, ce); - } - } - - in_dispatch_input_event = false; - } -} - -void DisplayServerOSX::release_rendering_thread() { -} - -void DisplayServerOSX::make_rendering_thread() { -} - -void DisplayServerOSX::swap_buffers() { #if defined(GLES3_ENABLED) - gl_manager->swap_buffers(); + drivers.push_back("opengl3"); #endif -} -void DisplayServerOSX::console_set_visible(bool p_enabled) { - //TODO - open terminal and redirect + return drivers; } -bool DisplayServerOSX::is_console_visible() const { - return isatty(STDIN_FILENO); +void DisplayServerOSX::register_osx_driver() { + register_create_function("osx", create_func, get_rendering_drivers_func); } DisplayServerOSX::DisplayServerOSX(const String &p_rendering_driver, WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { Input::get_singleton()->set_event_dispatch_function(_dispatch_input_events); r_error = OK; - drop_events = false; memset(cursors, 0, sizeof(cursors)); - cursor_shape = CURSOR_ARROW; - - key_event_pos = 0; - mouse_mode = MOUSE_MODE_VISIBLE; - autoreleasePool = [[NSAutoreleasePool alloc] init]; + event_source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); + ERR_FAIL_COND(!event_source); - eventSource = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); - ERR_FAIL_COND(!eventSource); + CGEventSourceSetLocalEventsSuppressionInterval(event_source, 0.0); - CGEventSourceSetLocalEventsSuppressionInterval(eventSource, 0.0); - - keyboard_layout_dirty = true; - displays_arrangement_dirty = true; - displays_scale_dirty = true; + int screen_count = get_screen_count(); + for (int i = 0; i < screen_count; i++) { + display_max_scale = fmax(display_max_scale, screen_get_scale(i)); + } - // Register to be notified on keyboard layout changes + // Register to be notified on keyboard layout changes. CFNotificationCenterAddObserver(CFNotificationCenterGetDistributedCenter(), - nullptr, keyboard_layout_changed, + nullptr, _keyboard_layout_changed, kTISNotifySelectedKeyboardInputSourceChanged, nullptr, CFNotificationSuspensionBehaviorDeliverImmediately); - // Register to be notified on displays arrangement changes - CGDisplayRegisterReconfigurationCallback(displays_arrangement_changed, nullptr); + // Register to be notified on displays arrangement changes. + CGDisplayRegisterReconfigurationCallback(_displays_arrangement_changed, nullptr); NSMenuItem *menu_item; NSString *title; @@ -3730,11 +2455,11 @@ DisplayServerOSX::DisplayServerOSX(const String &p_rendering_driver, WindowMode nsappname = [[NSProcessInfo processInfo] processName]; } - // Setup Dock menu + // Setup Dock menu. dock_menu = [[NSMenu alloc] initWithTitle:@"_dock"]; - // Setup Apple menu - apple_menu = [[[NSMenu alloc] initWithTitle:@""] autorelease]; + // Setup Apple menu. + apple_menu = [[NSMenu alloc] initWithTitle:@""]; title = [NSString stringWithFormat:NSLocalizedString(@"About %@", nil), nsappname]; [apple_menu addItemWithTitle:title action:@selector(showAbout:) keyEquivalent:@""]; @@ -3744,7 +2469,6 @@ DisplayServerOSX::DisplayServerOSX(const String &p_rendering_driver, WindowMode menu_item = [apple_menu addItemWithTitle:NSLocalizedString(@"Services", nil) action:nil keyEquivalent:@""]; [apple_menu setSubmenu:services forItem:menu_item]; [NSApp setServicesMenu:services]; - [services release]; [apple_menu addItem:[NSMenuItem separatorItem]]; @@ -3761,7 +2485,7 @@ DisplayServerOSX::DisplayServerOSX(const String &p_rendering_driver, WindowMode title = [NSString stringWithFormat:NSLocalizedString(@"Quit %@", nil), nsappname]; [apple_menu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"]; - // Add items to the menu bar + // Add items to the menu bar. NSMenu *main_menu = [NSApp mainMenu]; menu_item = [main_menu addItemWithTitle:@"" action:nil keyEquivalent:@""]; [main_menu setSubmenu:apple_menu forItem:menu_item]; @@ -3823,15 +2547,7 @@ DisplayServerOSX::DisplayServerOSX(const String &p_rendering_driver, WindowMode } DisplayServerOSX::~DisplayServerOSX() { - if (dock_menu) { - [dock_menu release]; - } - - for (Map<String, NSMenu *>::Element *E = submenu.front(); E; E = E->next()) { - [E->get() release]; - } - - //destroy all windows + // Destroy all windows. for (Map<WindowID, WindowData>::Element *E = windows.front(); E;) { Map<WindowID, WindowData>::Element *F = E; E = E->next(); @@ -3839,7 +2555,7 @@ DisplayServerOSX::~DisplayServerOSX() { [F->get().window_object close]; } - //destroy drivers + // Destroy drivers. #if defined(GLES3_ENABLED) if (gl_manager) { memdelete(gl_manager); @@ -3860,11 +2576,7 @@ DisplayServerOSX::~DisplayServerOSX() { #endif CFNotificationCenterRemoveObserver(CFNotificationCenterGetDistributedCenter(), nullptr, kTISNotifySelectedKeyboardInputSourceChanged, nullptr); - CGDisplayRemoveReconfigurationCallback(displays_arrangement_changed, nullptr); + CGDisplayRemoveReconfigurationCallback(_displays_arrangement_changed, nullptr); cursors_cache.clear(); } - -void DisplayServerOSX::register_osx_driver() { - register_create_function("osx", create_func, get_rendering_drivers_func); -} diff --git a/platform/osx/export/codesign.cpp b/platform/osx/export/codesign.cpp new file mode 100644 index 0000000000..8ea6ff519d --- /dev/null +++ b/platform/osx/export/codesign.cpp @@ -0,0 +1,1564 @@ +/*************************************************************************/ +/* codesign.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "codesign.h" + +#include "lipo.h" +#include "macho.h" +#include "plist.h" + +#include "core/os/os.h" +#include "editor/editor_settings.h" +#include "modules/modules_enabled.gen.h" // For regex. + +#include <ctime> + +#ifdef MODULE_REGEX_ENABLED + +/*************************************************************************/ +/* CodeSignCodeResources */ +/*************************************************************************/ + +String CodeSignCodeResources::hash_sha1_base64(const String &p_path) { + FileAccessRef fa = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(!fa, String(), vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path)); + + CryptoCore::SHA1Context ctx; + ctx.start(); + + unsigned char step[4096]; + while (true) { + uint64_t br = fa->get_buffer(step, 4096); + if (br > 0) { + ctx.update(step, br); + } + if (br < 4096) { + break; + } + } + + unsigned char hash[0x14]; + ctx.finish(hash); + fa->close(); + + return CryptoCore::b64_encode_str(hash, 0x14); +} + +String CodeSignCodeResources::hash_sha256_base64(const String &p_path) { + FileAccessRef fa = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(!fa, String(), vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path)); + + CryptoCore::SHA256Context ctx; + ctx.start(); + + unsigned char step[4096]; + while (true) { + uint64_t br = fa->get_buffer(step, 4096); + if (br > 0) { + ctx.update(step, br); + } + if (br < 4096) { + break; + } + } + + unsigned char hash[0x20]; + ctx.finish(hash); + fa->close(); + + return CryptoCore::b64_encode_str(hash, 0x20); +} + +void CodeSignCodeResources::add_rule1(const String &p_rule, const String &p_key, int p_weight, bool p_store) { + rules1.push_back(CRRule(p_rule, p_key, p_weight, p_store)); +} + +void CodeSignCodeResources::add_rule2(const String &p_rule, const String &p_key, int p_weight, bool p_store) { + rules2.push_back(CRRule(p_rule, p_key, p_weight, p_store)); +} + +CodeSignCodeResources::CRMatch CodeSignCodeResources::match_rules1(const String &p_path) const { + CRMatch found = CRMatch::CR_MATCH_NO; + int weight = 0; + for (int i = 0; i < rules1.size(); i++) { + RegEx regex = RegEx(rules1[i].file_pattern); + if (regex.search(p_path).is_valid()) { + if (rules1[i].key == "omit") { + return CRMatch::CR_MATCH_NO; + } else if (rules1[i].key == "nested") { + if (weight <= rules1[i].weight) { + found = CRMatch::CR_MATCH_NESTED; + weight = rules1[i].weight; + } + } else if (rules1[i].key == "optional") { + if (weight <= rules1[i].weight) { + found = CRMatch::CR_MATCH_OPTIONAL; + weight = rules1[i].weight; + } + } else { + if (weight <= rules1[i].weight) { + found = CRMatch::CR_MATCH_YES; + weight = rules1[i].weight; + } + } + } + } + return found; +} + +CodeSignCodeResources::CRMatch CodeSignCodeResources::match_rules2(const String &p_path) const { + CRMatch found = CRMatch::CR_MATCH_NO; + int weight = 0; + for (int i = 0; i < rules2.size(); i++) { + RegEx regex = RegEx(rules2[i].file_pattern); + if (regex.search(p_path).is_valid()) { + if (rules2[i].key == "omit") { + return CRMatch::CR_MATCH_NO; + } else if (rules2[i].key == "nested") { + if (weight <= rules2[i].weight) { + found = CRMatch::CR_MATCH_NESTED; + weight = rules2[i].weight; + } + } else if (rules2[i].key == "optional") { + if (weight <= rules2[i].weight) { + found = CRMatch::CR_MATCH_OPTIONAL; + weight = rules2[i].weight; + } + } else { + if (weight <= rules2[i].weight) { + found = CRMatch::CR_MATCH_YES; + weight = rules2[i].weight; + } + } + } + } + return found; +} + +bool CodeSignCodeResources::add_file1(const String &p_root, const String &p_path) { + CRMatch found = match_rules1(p_path); + if (found != CRMatch::CR_MATCH_YES && found != CRMatch::CR_MATCH_OPTIONAL) { + return true; // No match. + } + + CRFile f; + f.name = p_path; + f.optional = (found == CRMatch::CR_MATCH_OPTIONAL); + f.nested = false; + f.hash = hash_sha1_base64(p_root.plus_file(p_path)); + print_verbose(vformat("CodeSign/CodeResources: File(V1) %s hash1:%s", f.name, f.hash)); + + files1.push_back(f); + return true; +} + +bool CodeSignCodeResources::add_file2(const String &p_root, const String &p_path) { + CRMatch found = match_rules2(p_path); + if (found == CRMatch::CR_MATCH_NESTED) { + return add_nested_file(p_root, p_path, p_root.plus_file(p_path)); + } + if (found != CRMatch::CR_MATCH_YES && found != CRMatch::CR_MATCH_OPTIONAL) { + return true; // No match. + } + + CRFile f; + f.name = p_path; + f.optional = (found == CRMatch::CR_MATCH_OPTIONAL); + f.nested = false; + f.hash = hash_sha1_base64(p_root.plus_file(p_path)); + f.hash2 = hash_sha256_base64(p_root.plus_file(p_path)); + + print_verbose(vformat("CodeSign/CodeResources: File(V2) %s hash1:%s hash2:%s", f.name, f.hash, f.hash2)); + + files2.push_back(f); + return true; +} + +bool CodeSignCodeResources::add_nested_file(const String &p_root, const String &p_path, const String &p_exepath) { +#define CLEANUP() \ + if (files_to_add.size() > 1) { \ + for (int j = 0; j < files_to_add.size(); j++) { \ + da->remove(files_to_add[j]); \ + } \ + } + + DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + ERR_FAIL_COND_V(!da, false); + + Vector<String> files_to_add; + if (LipO::is_lipo(p_exepath)) { + String tmp_path_name = EditorPaths::get_singleton()->get_cache_dir().plus_file("_lipo"); + Error err = da->make_dir_recursive(tmp_path_name); + if (err != OK) { + ERR_FAIL_V_MSG(false, vformat("CodeSign/CodeResources: Failed to create \"%s\" subfolder.", tmp_path_name)); + } + LipO lip; + if (lip.open_file(p_exepath)) { + for (int i = 0; i < lip.get_arch_count(); i++) { + if (!lip.extract_arch(i, tmp_path_name.plus_file("_rqexe_" + itos(i)))) { + CLEANUP(); + ERR_FAIL_V_MSG(false, "CodeSign/CodeResources: Failed to extract thin binary."); + } + files_to_add.push_back(tmp_path_name.plus_file("_rqexe_" + itos(i))); + } + } + } else if (MachO::is_macho(p_exepath)) { + files_to_add.push_back(p_exepath); + } + + CRFile f; + f.name = p_path; + f.optional = false; + f.nested = true; + for (int i = 0; i < files_to_add.size(); i++) { + MachO mh; + if (!mh.open_file(files_to_add[i])) { + CLEANUP(); + ERR_FAIL_V_MSG(false, "CodeSign/CodeResources: Invalid executable file."); + } + PackedByteArray hash = mh.get_cdhash_sha256(); // Use SHA-256 variant, if available. + if (hash.size() != 0x20) { + hash = mh.get_cdhash_sha1(); // Use SHA-1 instead. + if (hash.size() != 0x14) { + CLEANUP(); + ERR_FAIL_V_MSG(false, "CodeSign/CodeResources: Unsigned nested executable file."); + } + } + hash.resize(0x14); // Always clamp to 0x14 size. + f.hash = CryptoCore::b64_encode_str(hash.ptr(), hash.size()); + + PackedByteArray rq_blob = mh.get_requirements(); + String req_string; + if (rq_blob.size() > 8) { + CodeSignRequirements rq = CodeSignRequirements(rq_blob); + Vector<String> rqs = rq.parse_requirements(); + for (int j = 0; j < rqs.size(); j++) { + if (rqs[j].begins_with("designated => ")) { + req_string = rqs[j].replace("designated => ", ""); + } + } + } + if (req_string.is_empty()) { + req_string = "cdhash H\"" + String::hex_encode_buffer(hash.ptr(), hash.size()) + "\""; + } + print_verbose(vformat("CodeSign/CodeResources: Nested object %s (cputype: %d) cdhash:%s designated rq:%s", f.name, mh.get_cputype(), f.hash, req_string)); + if (f.requirements != req_string) { + if (i != 0) { + f.requirements += " or "; + } + f.requirements += req_string; + } + } + files2.push_back(f); + + CLEANUP(); + return true; + +#undef CLEANUP +} + +bool CodeSignCodeResources::add_folder_recursive(const String &p_root, const String &p_path, const String &p_main_exe_path) { + DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + ERR_FAIL_COND_V(!da, false); + Error err = da->change_dir(p_root.plus_file(p_path)); + ERR_FAIL_COND_V(err != OK, false); + + bool ret = true; + da->list_dir_begin(); + String n = da->get_next(); + while (n != String()) { + if (n != "." && n != "..") { + String path = p_root.plus_file(p_path).plus_file(n); + if (path == p_main_exe_path) { + n = da->get_next(); + continue; // Skip main executable. + } + if (da->current_is_dir()) { + CRMatch found = match_rules2(p_path.plus_file(n)); + String fmw_ver = "Current"; // Framework version (default). + String info_path; + String main_exe; + bool bundle = false; + if (da->file_exists(path.plus_file("Contents/Info.plist"))) { + info_path = path.plus_file("Contents/Info.plist"); + main_exe = path.plus_file("Contents/MacOS"); + bundle = true; + } else if (da->file_exists(path.plus_file(vformat("Versions/%s/Resources/Info.plist", fmw_ver)))) { + info_path = path.plus_file(vformat("Versions/%s/Resources/Info.plist", fmw_ver)); + main_exe = path.plus_file(vformat("Versions/%s", fmw_ver)); + bundle = true; + } else if (da->file_exists(path.plus_file("Info.plist"))) { + info_path = path.plus_file("Info.plist"); + main_exe = path; + bundle = true; + } + if (bundle && found == CRMatch::CR_MATCH_NESTED && !info_path.is_empty()) { + // Read Info.plist. + PList info_plist; + if (info_plist.load_file(info_path)) { + if (info_plist.get_root()->data_type == PList::PLNodeType::PL_NODE_TYPE_DICT && info_plist.get_root()->data_dict.has("CFBundleExecutable")) { + main_exe = main_exe.plus_file(String::utf8(info_plist.get_root()->data_dict["CFBundleExecutable"]->data_string.get_data())); + } else { + ERR_FAIL_V_MSG(false, "CodeSign/CodeResources: Invalid Info.plist, no exe name."); + } + } else { + ERR_FAIL_V_MSG(false, "CodeSign/CodeResources: Invalid Info.plist, can't load."); + } + ret = ret && add_nested_file(p_root, p_path.plus_file(n), main_exe); + } else { + ret = ret && add_folder_recursive(p_root, p_path.plus_file(n), p_main_exe_path); + } + } else { + ret = ret && add_file1(p_root, p_path.plus_file(n)); + ret = ret && add_file2(p_root, p_path.plus_file(n)); + } + } + + n = da->get_next(); + } + + da->list_dir_end(); + return ret; +} + +bool CodeSignCodeResources::save_to_file(const String &p_path) { + PList pl; + + print_verbose(vformat("CodeSign/CodeResources: Writing to file: %s", p_path)); + + // Write version 1 hashes. + Ref<PListNode> files1_dict = PListNode::new_dict(); + pl.get_root()->push_subnode(files1_dict, "files"); + for (int i = 0; i < files1.size(); i++) { + if (files1[i].optional) { + Ref<PListNode> file_dict = PListNode::new_dict(); + files1_dict->push_subnode(file_dict, files1[i].name); + + file_dict->push_subnode(PListNode::new_data(files1[i].hash), "hash"); + file_dict->push_subnode(PListNode::new_bool(true), "optional"); + } else { + files1_dict->push_subnode(PListNode::new_data(files1[i].hash), files1[i].name); + } + } + + // Write version 2 hashes. + Ref<PListNode> files2_dict = PListNode::new_dict(); + pl.get_root()->push_subnode(files2_dict, "files2"); + for (int i = 0; i < files2.size(); i++) { + Ref<PListNode> file_dict = PListNode::new_dict(); + files2_dict->push_subnode(file_dict, files2[i].name); + + if (files2[i].nested) { + file_dict->push_subnode(PListNode::new_data(files2[i].hash), "cdhash"); + file_dict->push_subnode(PListNode::new_string(files2[i].requirements), "requirement"); + } else { + file_dict->push_subnode(PListNode::new_data(files2[i].hash), "hash"); + file_dict->push_subnode(PListNode::new_data(files2[i].hash2), "hash2"); + if (files2[i].optional) { + file_dict->push_subnode(PListNode::new_bool(true), "optional"); + } + } + } + + // Write version 1 rules. + Ref<PListNode> rules1_dict = PListNode::new_dict(); + pl.get_root()->push_subnode(rules1_dict, "rules"); + for (int i = 0; i < rules1.size(); i++) { + if (rules1[i].store) { + if (rules1[i].key.is_empty() && rules1[i].weight <= 0) { + rules1_dict->push_subnode(PListNode::new_bool(true), rules1[i].file_pattern); + } else { + Ref<PListNode> rule_dict = PListNode::new_dict(); + rules1_dict->push_subnode(rule_dict, rules1[i].file_pattern); + if (!rules1[i].key.is_empty()) { + rule_dict->push_subnode(PListNode::new_bool(true), rules1[i].key); + } + if (rules1[i].weight != 1) { + rule_dict->push_subnode(PListNode::new_real(rules1[i].weight), "weight"); + } + } + } + } + + // Write version 2 rules. + Ref<PListNode> rules2_dict = PListNode::new_dict(); + pl.get_root()->push_subnode(rules2_dict, "rules2"); + for (int i = 0; i < rules2.size(); i++) { + if (rules2[i].store) { + if (rules2[i].key.is_empty() && rules2[i].weight <= 0) { + rules2_dict->push_subnode(PListNode::new_bool(true), rules2[i].file_pattern); + } else { + Ref<PListNode> rule_dict = PListNode::new_dict(); + rules2_dict->push_subnode(rule_dict, rules2[i].file_pattern); + if (!rules2[i].key.is_empty()) { + rule_dict->push_subnode(PListNode::new_bool(true), rules2[i].key); + } + if (rules2[i].weight != 1) { + rule_dict->push_subnode(PListNode::new_real(rules2[i].weight), "weight"); + } + } + } + } + String text = pl.save_text(); + ERR_FAIL_COND_V_MSG(text.is_empty(), false, "CodeSign/CodeResources: Generating resources PList failed."); + + FileAccessRef fa = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(!fa, false, vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path)); + + CharString cs = text.utf8(); + fa->store_buffer((const uint8_t *)cs.ptr(), cs.length()); + fa->close(); + return true; +} + +/*************************************************************************/ +/* CodeSignRequirements */ +/*************************************************************************/ + +CodeSignRequirements::CodeSignRequirements() { + blob.append_array({ 0xFA, 0xDE, 0x0C, 0x01 }); // Requirement set magic. + blob.append_array({ 0x00, 0x00, 0x00, 0x0C }); // Length of requirements set (12 bytes). + blob.append_array({ 0x00, 0x00, 0x00, 0x00 }); // Empty. +} + +CodeSignRequirements::CodeSignRequirements(const PackedByteArray &p_data) { + blob = p_data; +} + +_FORCE_INLINE_ void CodeSignRequirements::_parse_certificate_slot(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const { +#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x)) + ERR_FAIL_COND_MSG(r_pos >= p_rq_size, "CodeSign/Requirements: Out of bounds."); + r_out += "certificate "; + uint32_t tag_slot = _R(r_pos); + if (tag_slot == 0x00000000) { + r_out += "leaf"; + } else if (tag_slot == 0xffffffff) { + r_out += "root"; + } else { + r_out += itos((int32_t)tag_slot); + } + r_pos += 4; +#undef _R +} + +_FORCE_INLINE_ void CodeSignRequirements::_parse_key(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const { +#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x)) + ERR_FAIL_COND_MSG(r_pos >= p_rq_size, "CodeSign/Requirements: Out of bounds."); + uint32_t key_size = _R(r_pos); + ERR_FAIL_COND_MSG(r_pos + key_size > p_rq_size, "CodeSign/Requirements: Out of bounds."); + CharString key; + key.resize(key_size); + memcpy(key.ptrw(), blob.ptr() + r_pos + 4, key_size); + r_pos += 4 + key_size + PAD(key_size, 4); + r_out += "[" + String::utf8(key, key_size) + "]"; +#undef _R +} + +_FORCE_INLINE_ void CodeSignRequirements::_parse_oid_key(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const { +#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x)) + ERR_FAIL_COND_MSG(r_pos >= p_rq_size, "CodeSign/Requirements: Out of bounds."); + uint32_t key_size = _R(r_pos); + ERR_FAIL_COND_MSG(r_pos + key_size > p_rq_size, "CodeSign/Requirements: Out of bounds."); + r_out += "[field."; + r_out += itos(blob[r_pos + 4] / 40) + "."; + r_out += itos(blob[r_pos + 4] % 40); + uint32_t spos = r_pos + 5; + while (spos < r_pos + 4 + key_size) { + r_out += "."; + if (blob[spos] <= 127) { + r_out += itos(blob[spos]); + spos += 1; + } else { + uint32_t x = (0x7F & blob[spos]) << 7; + spos += 1; + while (blob[spos] > 127) { + x = (x + (0x7F & blob[spos])) << 7; + spos += 1; + } + x = (x + (0x7F & blob[spos])); + r_out += itos(x); + spos += 1; + } + } + r_out += "]"; + r_pos += 4 + key_size + PAD(key_size, 4); +#undef _R +} + +_FORCE_INLINE_ void CodeSignRequirements::_parse_hash_string(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const { +#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x)) + ERR_FAIL_COND_MSG(r_pos >= p_rq_size, "CodeSign/Requirements: Out of bounds."); + uint32_t tag_size = _R(r_pos); + ERR_FAIL_COND_MSG(r_pos + tag_size > p_rq_size, "CodeSign/Requirements: Out of bounds."); + PackedByteArray data; + data.resize(tag_size); + memcpy(data.ptrw(), blob.ptr() + r_pos + 4, tag_size); + r_out += "H\"" + String::hex_encode_buffer(data.ptr(), data.size()) + "\""; + r_pos += 4 + tag_size + PAD(tag_size, 4); +#undef _R +} + +_FORCE_INLINE_ void CodeSignRequirements::_parse_value(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const { +#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x)) + ERR_FAIL_COND_MSG(r_pos >= p_rq_size, "CodeSign/Requirements: Out of bounds."); + uint32_t key_size = _R(r_pos); + ERR_FAIL_COND_MSG(r_pos + key_size > p_rq_size, "CodeSign/Requirements: Out of bounds."); + CharString key; + key.resize(key_size); + memcpy(key.ptrw(), blob.ptr() + r_pos + 4, key_size); + r_pos += 4 + key_size + PAD(key_size, 4); + r_out += "\"" + String::utf8(key, key_size) + "\""; +#undef _R +} + +_FORCE_INLINE_ void CodeSignRequirements::_parse_date(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const { +#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x)) + ERR_FAIL_COND_MSG(r_pos >= p_rq_size, "CodeSign/Requirements: Out of bounds."); + uint32_t date = _R(r_pos); + time_t t = 978307200 + date; + struct tm lt; +#ifdef WINDOWS_ENABLED + gmtime_s(<, &t); +#else + gmtime_r(&t, <); +#endif + r_out += vformat("<%04d-%02d-%02d ", (int)(1900 + lt.tm_year), (int)(lt.tm_mon + 1), (int)(lt.tm_mday)) + vformat("%02d:%02d:%02d +0000>", (int)(lt.tm_hour), (int)(lt.tm_min), (int)(lt.tm_sec)); +#undef _R +} + +_FORCE_INLINE_ bool CodeSignRequirements::_parse_match(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const { +#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x)) + ERR_FAIL_COND_V_MSG(r_pos >= p_rq_size, false, "CodeSign/Requirements: Out of bounds."); + uint32_t match = _R(r_pos); + r_pos += 4; + switch (match) { + case 0x00000000: { + r_out += "exists"; + } break; + case 0x00000001: { + r_out += "= "; + _parse_value(r_pos, r_out, p_rq_size); + } break; + case 0x00000002: { + r_out += "~ "; + _parse_value(r_pos, r_out, p_rq_size); + } break; + case 0x00000003: { + r_out += "= *"; + _parse_value(r_pos, r_out, p_rq_size); + } break; + case 0x00000004: { + r_out += "= "; + _parse_value(r_pos, r_out, p_rq_size); + r_out += "*"; + } break; + case 0x00000005: { + r_out += "< "; + _parse_value(r_pos, r_out, p_rq_size); + } break; + case 0x00000006: { + r_out += "> "; + _parse_value(r_pos, r_out, p_rq_size); + } break; + case 0x00000007: { + r_out += "<= "; + _parse_value(r_pos, r_out, p_rq_size); + } break; + case 0x00000008: { + r_out += ">= "; + _parse_value(r_pos, r_out, p_rq_size); + } break; + case 0x00000009: { + r_out += "= "; + _parse_date(r_pos, r_out, p_rq_size); + } break; + case 0x0000000A: { + r_out += "< "; + _parse_date(r_pos, r_out, p_rq_size); + } break; + case 0x0000000B: { + r_out += "> "; + _parse_date(r_pos, r_out, p_rq_size); + } break; + case 0x0000000C: { + r_out += "<= "; + _parse_date(r_pos, r_out, p_rq_size); + } break; + case 0x0000000D: { + r_out += ">= "; + _parse_date(r_pos, r_out, p_rq_size); + } break; + case 0x0000000E: { + r_out += "absent"; + } break; + default: { + return false; + } + } + return true; +#undef _R +} + +Vector<String> CodeSignRequirements::parse_requirements() const { +#define _R(x) BSWAP32(*(uint32_t *)(blob.ptr() + x)) + Vector<String> list; + + // Read requirements set header. + ERR_FAIL_COND_V_MSG(blob.size() < 12, list, "CodeSign/Requirements: Blob is too small."); + uint32_t magic = _R(0); + ERR_FAIL_COND_V_MSG(magic != 0xfade0c01, list, "CodeSign/Requirements: Invalid set magic."); + uint32_t size = _R(4); + ERR_FAIL_COND_V_MSG(size != (uint32_t)blob.size(), list, "CodeSign/Requirements: Invalid set size."); + uint32_t count = _R(8); + + for (uint32_t i = 0; i < count; i++) { + String out; + + // Read requirement header. + uint32_t rq_type = _R(12 + i * 8); + uint32_t rq_offset = _R(12 + i * 8 + 4); + ERR_FAIL_COND_V_MSG(rq_offset + 12 >= (uint32_t)blob.size(), list, "CodeSign/Requirements: Invalid requirement offset."); + switch (rq_type) { + case 0x00000001: { + out += "host => "; + } break; + case 0x00000002: { + out += "guest => "; + } break; + case 0x00000003: { + out += "designated => "; + } break; + case 0x00000004: { + out += "library => "; + } break; + case 0x00000005: { + out += "plugin => "; + } break; + default: { + ERR_FAIL_V_MSG(list, "CodeSign/Requirements: Invalid requirement type."); + } + } + uint32_t rq_magic = _R(rq_offset); + uint32_t rq_size = _R(rq_offset + 4); + uint32_t rq_ver = _R(rq_offset + 8); + uint32_t pos = rq_offset + 12; + ERR_FAIL_COND_V_MSG(rq_magic != 0xfade0c00, list, "CodeSign/Requirements: Invalid requirement magic."); + ERR_FAIL_COND_V_MSG(rq_ver != 0x00000001, list, "CodeSign/Requirements: Invalid requirement version."); + + // Read requirement tokens. + List<String> tokens; + while (pos < rq_offset + rq_size) { + uint32_t rq_tag = _R(pos); + pos += 4; + String token; + switch (rq_tag) { + case 0x00000000: { + token = "false"; + } break; + case 0x00000001: { + token = "true"; + } break; + case 0x00000002: { + token = "identifier "; + _parse_value(pos, token, rq_offset + rq_size); + } break; + case 0x00000003: { + token = "anchor apple"; + } break; + case 0x00000004: { + _parse_certificate_slot(pos, token, rq_offset + rq_size); + token += " "; + _parse_hash_string(pos, token, rq_offset + rq_size); + } break; + case 0x00000005: { + token = "info"; + _parse_key(pos, token, rq_offset + rq_size); + token += " = "; + _parse_value(pos, token, rq_offset + rq_size); + } break; + case 0x00000006: { + token = "and"; + } break; + case 0x00000007: { + token = "or"; + } break; + case 0x00000008: { + token = "cdhash "; + _parse_hash_string(pos, token, rq_offset + rq_size); + } break; + case 0x00000009: { + token = "!"; + } break; + case 0x0000000A: { + token = "info"; + _parse_key(pos, token, rq_offset + rq_size); + token += " "; + ERR_FAIL_COND_V_MSG(!_parse_match(pos, token, rq_offset + rq_size), list, "CodeSign/Requirements: Unsupported match suffix."); + } break; + case 0x0000000B: { + _parse_certificate_slot(pos, token, rq_offset + rq_size); + _parse_key(pos, token, rq_offset + rq_size); + token += " "; + ERR_FAIL_COND_V_MSG(!_parse_match(pos, token, rq_offset + rq_size), list, "CodeSign/Requirements: Unsupported match suffix."); + } break; + case 0x0000000C: { + _parse_certificate_slot(pos, token, rq_offset + rq_size); + token += " trusted"; + } break; + case 0x0000000D: { + token = "anchor trusted"; + } break; + case 0x0000000E: { + _parse_certificate_slot(pos, token, rq_offset + rq_size); + _parse_oid_key(pos, token, rq_offset + rq_size); + token += " "; + ERR_FAIL_COND_V_MSG(!_parse_match(pos, token, rq_offset + rq_size), list, "CodeSign/Requirements: Unsupported match suffix."); + } break; + case 0x0000000F: { + token = "anchor apple generic"; + } break; + default: { + ERR_FAIL_V_MSG(list, "CodeSign/Requirements: Invalid requirement token."); + } break; + } + tokens.push_back(token); + } + + // Polish to infix notation (w/o bracket optimization). + for (List<String>::Element *E = tokens.back(); E; E = E->prev()) { + if (E->get() == "and") { + ERR_FAIL_COND_V_MSG(!E->next() || !E->next()->next(), list, "CodeSign/Requirements: Invalid token sequence."); + String token = "(" + E->next()->get() + " and " + E->next()->next()->get() + ")"; + tokens.erase(E->next()->next()); + tokens.erase(E->next()); + E->get() = token; + } else if (E->get() == "or") { + ERR_FAIL_COND_V_MSG(!E->next() || !E->next()->next(), list, "CodeSign/Requirements: Invalid token sequence."); + String token = "(" + E->next()->get() + " or " + E->next()->next()->get() + ")"; + tokens.erase(E->next()->next()); + tokens.erase(E->next()); + E->get() = token; + } + } + + if (tokens.size() == 1) { + list.push_back(out + tokens.front()->get()); + } else { + ERR_FAIL_V_MSG(list, "CodeSign/Requirements: Invalid token sequence."); + } + } + + return list; +#undef _R +} + +PackedByteArray CodeSignRequirements::get_hash_sha1() const { + PackedByteArray hash; + hash.resize(0x14); + + CryptoCore::SHA1Context ctx; + ctx.start(); + ctx.update(blob.ptr(), blob.size()); + ctx.finish(hash.ptrw()); + + return hash; +} + +PackedByteArray CodeSignRequirements::get_hash_sha256() const { + PackedByteArray hash; + hash.resize(0x20); + + CryptoCore::SHA256Context ctx; + ctx.start(); + ctx.update(blob.ptr(), blob.size()); + ctx.finish(hash.ptrw()); + + return hash; +} + +int CodeSignRequirements::get_size() const { + return blob.size(); +} + +void CodeSignRequirements::write_to_file(FileAccess *p_file) const { + ERR_FAIL_COND_MSG(!p_file, "CodeSign/Requirements: Invalid file handle."); + p_file->store_buffer(blob.ptr(), blob.size()); +} + +/*************************************************************************/ +/* CodeSignEntitlementsText */ +/*************************************************************************/ + +CodeSignEntitlementsText::CodeSignEntitlementsText() { + blob.append_array({ 0xFA, 0xDE, 0x71, 0x71 }); // Text Entitlements set magic. + blob.append_array({ 0x00, 0x00, 0x00, 0x08 }); // Length (8 bytes). +} + +CodeSignEntitlementsText::CodeSignEntitlementsText(const String &p_string) { + CharString utf8 = p_string.utf8(); + blob.append_array({ 0xFA, 0xDE, 0x71, 0x71 }); // Text Entitlements set magic. + for (int i = 3; i >= 0; i--) { + uint8_t x = ((utf8.length() + 8) >> i * 8) & 0xFF; // Size. + blob.push_back(x); + } + for (int i = 0; i < utf8.length(); i++) { // Write data. + blob.push_back(utf8[i]); + } +} + +PackedByteArray CodeSignEntitlementsText::get_hash_sha1() const { + PackedByteArray hash; + hash.resize(0x14); + + CryptoCore::SHA1Context ctx; + ctx.start(); + ctx.update(blob.ptr(), blob.size()); + ctx.finish(hash.ptrw()); + + return hash; +} + +PackedByteArray CodeSignEntitlementsText::get_hash_sha256() const { + PackedByteArray hash; + hash.resize(0x20); + + CryptoCore::SHA256Context ctx; + ctx.start(); + ctx.update(blob.ptr(), blob.size()); + ctx.finish(hash.ptrw()); + + return hash; +} + +int CodeSignEntitlementsText::get_size() const { + return blob.size(); +} + +void CodeSignEntitlementsText::write_to_file(FileAccess *p_file) const { + ERR_FAIL_COND_MSG(!p_file, "CodeSign/EntitlementsText: Invalid file handle."); + p_file->store_buffer(blob.ptr(), blob.size()); +} + +/*************************************************************************/ +/* CodeSignEntitlementsBinary */ +/*************************************************************************/ + +CodeSignEntitlementsBinary::CodeSignEntitlementsBinary() { + blob.append_array({ 0xFA, 0xDE, 0x71, 0x72 }); // Binary Entitlements magic. + blob.append_array({ 0x00, 0x00, 0x00, 0x08 }); // Length (8 bytes). +} + +CodeSignEntitlementsBinary::CodeSignEntitlementsBinary(const String &p_string) { + PList pl = PList(p_string); + + PackedByteArray asn1 = pl.save_asn1(); + blob.append_array({ 0xFA, 0xDE, 0x71, 0x72 }); // Binary Entitlements magic. + uint32_t size = asn1.size() + 8; + for (int i = 3; i >= 0; i--) { + uint8_t x = (size >> i * 8) & 0xFF; // Size. + blob.push_back(x); + } + blob.append_array(asn1); // Write data. +} + +PackedByteArray CodeSignEntitlementsBinary::get_hash_sha1() const { + PackedByteArray hash; + hash.resize(0x14); + + CryptoCore::SHA1Context ctx; + ctx.start(); + ctx.update(blob.ptr(), blob.size()); + ctx.finish(hash.ptrw()); + + return hash; +} + +PackedByteArray CodeSignEntitlementsBinary::get_hash_sha256() const { + PackedByteArray hash; + hash.resize(0x20); + + CryptoCore::SHA256Context ctx; + ctx.start(); + ctx.update(blob.ptr(), blob.size()); + ctx.finish(hash.ptrw()); + + return hash; +} + +int CodeSignEntitlementsBinary::get_size() const { + return blob.size(); +} + +void CodeSignEntitlementsBinary::write_to_file(FileAccess *p_file) const { + ERR_FAIL_COND_MSG(!p_file, "CodeSign/EntitlementsBinary: Invalid file handle."); + p_file->store_buffer(blob.ptr(), blob.size()); +} + +/*************************************************************************/ +/* CodeSignCodeDirectory */ +/*************************************************************************/ + +CodeSignCodeDirectory::CodeSignCodeDirectory() { + blob.append_array({ 0xFA, 0xDE, 0x0C, 0x02 }); // Code Directory magic. + blob.append_array({ 0x00, 0x00, 0x00, 0x00 }); // Size (8 bytes). +} + +CodeSignCodeDirectory::CodeSignCodeDirectory(uint8_t p_hash_size, uint8_t p_hash_type, bool p_main, const CharString &p_id, const CharString &p_team_id, uint32_t p_page_size, uint64_t p_exe_limit, uint64_t p_code_limit) { + pages = p_code_limit / (uint64_t(1) << p_page_size); + remain = p_code_limit % (uint64_t(1) << p_page_size); + code_slots = pages + (remain > 0 ? 1 : 0); + special_slots = 7; + + int cd_size = 8 + sizeof(CodeDirectoryHeader) + (code_slots + special_slots) * p_hash_size + p_id.size() + p_team_id.size(); + int cd_off = 8 + sizeof(CodeDirectoryHeader); + blob.append_array({ 0xFA, 0xDE, 0x0C, 0x02 }); // Code Directory magic. + for (int i = 3; i >= 0; i--) { + uint8_t x = (cd_size >> i * 8) & 0xFF; // Size. + blob.push_back(x); + } + blob.resize(cd_size); + memset(blob.ptrw() + 8, 0x00, cd_size - 8); + CodeDirectoryHeader *cd = (CodeDirectoryHeader *)(blob.ptrw() + 8); + + bool is_64_cl = (p_code_limit >= std::numeric_limits<uint32_t>::max()); + + // Version and options. + cd->version = BSWAP32(0x20500); + cd->flags = BSWAP32(SIGNATURE_ADHOC | SIGNATURE_RUNTIME); + cd->special_slots = BSWAP32(special_slots); + cd->code_slots = BSWAP32(code_slots); + if (is_64_cl) { + cd->code_limit_64 = BSWAP64(p_code_limit); + } else { + cd->code_limit = BSWAP32(p_code_limit); + } + cd->hash_size = p_hash_size; + cd->hash_type = p_hash_type; + cd->page_size = p_page_size; + cd->exec_seg_base = 0x00; + cd->exec_seg_limit = BSWAP64(p_exe_limit); + cd->exec_seg_flags = 0; + if (p_main) { + cd->exec_seg_flags |= EXECSEG_MAIN_BINARY; + } + cd->exec_seg_flags = BSWAP64(cd->exec_seg_flags); + uint32_t version = (11 << 16) + (3 << 8) + 0; // Version 11.3.0 + cd->runtime = BSWAP32(version); + + // Copy ID. + cd->ident_offset = BSWAP32(cd_off); + memcpy(blob.ptrw() + cd_off, p_id.get_data(), p_id.size()); + cd_off += p_id.size(); + + // Copy Team ID. + if (p_team_id.length() > 0) { + cd->team_offset = BSWAP32(cd_off); + memcpy(blob.ptrw() + cd_off, p_team_id.get_data(), p_team_id.size()); + cd_off += p_team_id.size(); + } else { + cd->team_offset = 0; + } + + // Scatter vector. + cd->scatter_vector_offset = 0; // Not used. + + // Executable hashes offset. + cd->hash_offset = BSWAP32(cd_off + special_slots * cd->hash_size); +} + +bool CodeSignCodeDirectory::set_hash_in_slot(const PackedByteArray &p_hash, int p_slot) { + ERR_FAIL_COND_V_MSG((p_slot < -special_slots) || (p_slot >= code_slots), false, vformat("CodeSign/CodeDirectory: Invalid hash slot index: %d.", p_slot)); + CodeDirectoryHeader *cd = reinterpret_cast<CodeDirectoryHeader *>(blob.ptrw() + 8); + for (int i = 0; i < cd->hash_size; i++) { + blob.write[BSWAP32(cd->hash_offset) + p_slot * cd->hash_size + i] = p_hash[i]; + } + return true; +} + +int32_t CodeSignCodeDirectory::get_page_count() { + return pages; +} + +int32_t CodeSignCodeDirectory::get_page_remainder() { + return remain; +} + +PackedByteArray CodeSignCodeDirectory::get_hash_sha1() const { + PackedByteArray hash; + hash.resize(0x14); + + CryptoCore::SHA1Context ctx; + ctx.start(); + ctx.update(blob.ptr(), blob.size()); + ctx.finish(hash.ptrw()); + + return hash; +} + +PackedByteArray CodeSignCodeDirectory::get_hash_sha256() const { + PackedByteArray hash; + hash.resize(0x20); + + CryptoCore::SHA256Context ctx; + ctx.start(); + ctx.update(blob.ptr(), blob.size()); + ctx.finish(hash.ptrw()); + + return hash; +} + +int CodeSignCodeDirectory::get_size() const { + return blob.size(); +} + +void CodeSignCodeDirectory::write_to_file(FileAccess *p_file) const { + ERR_FAIL_COND_MSG(!p_file, "CodeSign/CodeDirectory: Invalid file handle."); + p_file->store_buffer(blob.ptr(), blob.size()); +} + +/*************************************************************************/ +/* CodeSignSignature */ +/*************************************************************************/ + +CodeSignSignature::CodeSignSignature() { + blob.append_array({ 0xFA, 0xDE, 0x0B, 0x01 }); // Signature magic. + uint32_t sign_size = 8; // Ad-hoc signature is empty. + for (int i = 3; i >= 0; i--) { + uint8_t x = (sign_size >> i * 8) & 0xFF; // Size. + blob.push_back(x); + } +} + +PackedByteArray CodeSignSignature::get_hash_sha1() const { + PackedByteArray hash; + hash.resize(0x14); + + CryptoCore::SHA1Context ctx; + ctx.start(); + ctx.update(blob.ptr(), blob.size()); + ctx.finish(hash.ptrw()); + + return hash; +} + +PackedByteArray CodeSignSignature::get_hash_sha256() const { + PackedByteArray hash; + hash.resize(0x20); + + CryptoCore::SHA256Context ctx; + ctx.start(); + ctx.update(blob.ptr(), blob.size()); + ctx.finish(hash.ptrw()); + + return hash; +} + +int CodeSignSignature::get_size() const { + return blob.size(); +} + +void CodeSignSignature::write_to_file(FileAccess *p_file) const { + ERR_FAIL_COND_MSG(!p_file, "CodeSign/Signature: Invalid file handle."); + p_file->store_buffer(blob.ptr(), blob.size()); +} + +/*************************************************************************/ +/* CodeSignSuperBlob */ +/*************************************************************************/ + +bool CodeSignSuperBlob::add_blob(const Ref<CodeSignBlob> &p_blob) { + if (p_blob.is_valid()) { + blobs.push_back(p_blob); + return true; + } else { + return false; + } +} + +int CodeSignSuperBlob::get_size() const { + int size = 12 + blobs.size() * 8; + for (int i = 0; i < blobs.size(); i++) { + if (blobs[i].is_null()) { + return 0; + } + size += blobs[i]->get_size(); + } + return size; +} + +void CodeSignSuperBlob::write_to_file(FileAccess *p_file) const { + ERR_FAIL_COND_MSG(!p_file, "CodeSign/SuperBlob: Invalid file handle."); + uint32_t size = get_size(); + uint32_t data_offset = 12 + blobs.size() * 8; + + // Write header. + p_file->store_32(BSWAP32(0xfade0cc0)); + p_file->store_32(BSWAP32(size)); + p_file->store_32(BSWAP32(blobs.size())); + + // Write index. + for (int i = 0; i < blobs.size(); i++) { + if (blobs[i].is_null()) { + return; + } + p_file->store_32(BSWAP32(blobs[i]->get_index_type())); + p_file->store_32(BSWAP32(data_offset)); + data_offset += blobs[i]->get_size(); + } + + // Write blobs. + for (int i = 0; i < blobs.size(); i++) { + blobs[i]->write_to_file(p_file); + } +} + +/*************************************************************************/ +/* CodeSign */ +/*************************************************************************/ + +PackedByteArray CodeSign::file_hash_sha1(const String &p_path) { + PackedByteArray file_hash; + FileAccessRef f = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(!f, PackedByteArray(), vformat("CodeSign: Can't open file: \"%s\".", p_path)); + + CryptoCore::SHA1Context ctx; + ctx.start(); + + unsigned char step[4096]; + while (true) { + uint64_t br = f->get_buffer(step, 4096); + if (br > 0) { + ctx.update(step, br); + } + if (br < 4096) { + break; + } + } + + file_hash.resize(0x14); + ctx.finish(file_hash.ptrw()); + return file_hash; +} + +PackedByteArray CodeSign::file_hash_sha256(const String &p_path) { + PackedByteArray file_hash; + FileAccessRef f = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(!f, PackedByteArray(), vformat("CodeSign: Can't open file: \"%s\".", p_path)); + + CryptoCore::SHA256Context ctx; + ctx.start(); + + unsigned char step[4096]; + while (true) { + uint64_t br = f->get_buffer(step, 4096); + if (br > 0) { + ctx.update(step, br); + } + if (br < 4096) { + break; + } + } + + file_hash.resize(0x20); + ctx.finish(file_hash.ptrw()); + return file_hash; +} + +Error CodeSign::_codesign_file(bool p_use_hardened_runtime, bool p_force, const String &p_info, const String &p_exe_path, const String &p_bundle_path, const String &p_ent_path, bool p_ios_bundle, String &r_error_msg) { +#define CLEANUP() \ + if (files_to_sign.size() > 1) { \ + for (int j = 0; j < files_to_sign.size(); j++) { \ + da->remove(files_to_sign[j]); \ + } \ + } + + print_verbose(vformat("CodeSign: Signing executable: %s, bundle: %s with entitlements %s", p_exe_path, p_bundle_path, p_ent_path)); + + PackedByteArray info_hash1, info_hash2; + PackedByteArray res_hash1, res_hash2; + String id; + String main_exe = p_exe_path; + + DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (!da) { + r_error_msg = TTR("Can't get filesystem access."); + ERR_FAIL_V_MSG(ERR_CANT_CREATE, "CodeSign: Can't get filesystem access."); + } + + // Read Info.plist. + if (!p_info.is_empty()) { + print_verbose(vformat("CodeSign: Reading bundle info...")); + PList info_plist; + if (info_plist.load_file(p_info)) { + info_hash1 = file_hash_sha1(p_info); + info_hash2 = file_hash_sha256(p_info); + if (info_hash1.is_empty() || info_hash2.is_empty()) { + r_error_msg = TTR("Failed to get Info.plist hash."); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Failed to get Info.plist hash."); + } + + if (info_plist.get_root()->data_type == PList::PLNodeType::PL_NODE_TYPE_DICT && info_plist.get_root()->data_dict.has("CFBundleExecutable")) { + main_exe = p_exe_path.plus_file(String::utf8(info_plist.get_root()->data_dict["CFBundleExecutable"]->data_string.get_data())); + } else { + r_error_msg = TTR("Invalid Info.plist, no exe name."); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Invalid Info.plist, no exe name."); + } + + if (info_plist.get_root()->data_type == PList::PLNodeType::PL_NODE_TYPE_DICT && info_plist.get_root()->data_dict.has("CFBundleIdentifier")) { + id = info_plist.get_root()->data_dict["CFBundleIdentifier"]->data_string.get_data(); + } else { + r_error_msg = TTR("Invalid Info.plist, no bundle id."); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Invalid Info.plist, no bundle id."); + } + } else { + r_error_msg = TTR("Invalid Info.plist, can't load."); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Invalid Info.plist, can't load."); + } + } + + // Extract fat binary. + Vector<String> files_to_sign; + if (LipO::is_lipo(main_exe)) { + print_verbose(vformat("CodeSign: Executable is fat, extracting...")); + String tmp_path_name = EditorPaths::get_singleton()->get_cache_dir().plus_file("_lipo"); + Error err = da->make_dir_recursive(tmp_path_name); + if (err != OK) { + r_error_msg = vformat(TTR("Failed to create \"%s\" subfolder."), tmp_path_name); + ERR_FAIL_V_MSG(FAILED, vformat("CodeSign: Failed to create \"%s\" subfolder.", tmp_path_name)); + } + LipO lip; + if (lip.open_file(main_exe)) { + for (int i = 0; i < lip.get_arch_count(); i++) { + if (!lip.extract_arch(i, tmp_path_name.plus_file("_exe_" + itos(i)))) { + CLEANUP(); + r_error_msg = TTR("Failed to extract thin binary."); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Failed to extract thin binary."); + } + files_to_sign.push_back(tmp_path_name.plus_file("_exe_" + itos(i))); + } + } + } else if (MachO::is_macho(main_exe)) { + print_verbose(vformat("CodeSign: Executable is thin...")); + files_to_sign.push_back(main_exe); + } else { + r_error_msg = TTR("Invalid binary format."); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Invalid binary format."); + } + + // Check if it's already signed. + if (!p_force) { + for (int i = 0; i < files_to_sign.size(); i++) { + MachO mh; + mh.open_file(files_to_sign[i]); + if (mh.is_signed()) { + CLEANUP(); + r_error_msg = TTR("Already signed!"); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Already signed!"); + } + } + } + + // Generate core resources. + if (!p_bundle_path.is_empty()) { + print_verbose(vformat("CodeSign: Generating bundle CodeResources...")); + CodeSignCodeResources cr; + + if (p_ios_bundle) { + cr.add_rule1("^.*"); + cr.add_rule1("^.*\\.lproj/", "optional", 100); + cr.add_rule1("^.*\\.lproj/locversion.plist$", "omit", 1100); + cr.add_rule1("^Base\\.lproj/", "", 1010); + cr.add_rule1("^version.plist$"); + + cr.add_rule2(".*\\.dSYM($|/)", "", 11); + cr.add_rule2("^(.*/)?\\.DS_Store$", "omit", 2000); + cr.add_rule2("^.*"); + cr.add_rule2("^.*\\.lproj/", "optional", 1000); + cr.add_rule2("^.*\\.lproj/locversion.plist$", "omit", 1100); + cr.add_rule2("^Base\\.lproj/", "", 1010); + cr.add_rule2("^Info\\.plist$", "omit", 20); + cr.add_rule2("^PkgInfo$", "omit", 20); + cr.add_rule2("^embedded\\.provisionprofile$", "", 10); + cr.add_rule2("^version\\.plist$", "", 20); + + cr.add_rule2("^_MASReceipt", "omit", 2000, false); + cr.add_rule2("^_CodeSignature", "omit", 2000, false); + cr.add_rule2("^CodeResources", "omit", 2000, false); + } else { + cr.add_rule1("^Resources/"); + cr.add_rule1("^Resources/.*\\.lproj/", "optional", 1000); + cr.add_rule1("^Resources/.*\\.lproj/locversion.plist$", "omit", 1100); + cr.add_rule1("^Resources/Base\\.lproj/", "", 1010); + cr.add_rule1("^version.plist$"); + + cr.add_rule2(".*\\.dSYM($|/)", "", 11); + cr.add_rule2("^(.*/)?\\.DS_Store$", "omit", 2000); + cr.add_rule2("^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/", "nested", 10); + cr.add_rule2("^.*"); + cr.add_rule2("^Info\\.plist$", "omit", 20); + cr.add_rule2("^PkgInfo$", "omit", 20); + cr.add_rule2("^Resources/", "", 20); + cr.add_rule2("^Resources/.*\\.lproj/", "optional", 1000); + cr.add_rule2("^Resources/.*\\.lproj/locversion.plist$", "omit", 1100); + cr.add_rule2("^Resources/Base\\.lproj/", "", 1010); + cr.add_rule2("^[^/]+$", "nested", 10); + cr.add_rule2("^embedded\\.provisionprofile$", "", 10); + cr.add_rule2("^version\\.plist$", "", 20); + cr.add_rule2("^_MASReceipt", "omit", 2000, false); + cr.add_rule2("^_CodeSignature", "omit", 2000, false); + cr.add_rule2("^CodeResources", "omit", 2000, false); + } + + if (!cr.add_folder_recursive(p_bundle_path, "", main_exe)) { + CLEANUP(); + r_error_msg = TTR("Failed to process nested resources."); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Failed to process nested resources."); + } + Error err = da->make_dir_recursive(p_bundle_path.plus_file("_CodeSignature")); + if (err != OK) { + CLEANUP(); + r_error_msg = TTR("Failed to create _CodeSignature subfolder."); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Failed to create _CodeSignature subfolder."); + } + cr.save_to_file(p_bundle_path.plus_file("_CodeSignature").plus_file("CodeResources")); + res_hash1 = file_hash_sha1(p_bundle_path.plus_file("_CodeSignature").plus_file("CodeResources")); + res_hash2 = file_hash_sha256(p_bundle_path.plus_file("_CodeSignature").plus_file("CodeResources")); + if (res_hash1.is_empty() || res_hash2.is_empty()) { + CLEANUP(); + r_error_msg = TTR("Failed to get CodeResources hash."); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Failed to get CodeResources hash."); + } + } + + // Generate common signature structures. + if (id.is_empty()) { + Ref<Crypto> crypto = Ref<Crypto>(Crypto::create()); + PackedByteArray uuid = crypto->generate_random_bytes(16); + id = (String("a-55554944") /*a-UUID*/ + String::hex_encode_buffer(uuid.ptr(), 16)); + } + CharString uuid_str = id.utf8(); + print_verbose(vformat("CodeSign: Used bundle ID: %s", id)); + + print_verbose(vformat("CodeSign: Processing entitlements...")); + + Ref<CodeSignEntitlementsText> cet; + Ref<CodeSignEntitlementsBinary> ceb; + if (!p_ent_path.is_empty()) { + String entitlements = FileAccess::get_file_as_string(p_ent_path); + if (entitlements.is_empty()) { + CLEANUP(); + r_error_msg = TTR("Invalid entitlements file."); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Invalid entitlements file."); + } + cet = Ref<CodeSignEntitlementsText>(memnew(CodeSignEntitlementsText(entitlements))); + ceb = Ref<CodeSignEntitlementsBinary>(memnew(CodeSignEntitlementsBinary(entitlements))); + } + + print_verbose(vformat("CodeSign: Generating requirements...")); + Ref<CodeSignRequirements> rq; + String team_id = ""; + rq = Ref<CodeSignRequirements>(memnew(CodeSignRequirements())); + + // Sign executables. + for (int i = 0; i < files_to_sign.size(); i++) { + MachO mh; + if (!mh.open_file(files_to_sign[i])) { + CLEANUP(); + r_error_msg = TTR("Invalid executable file."); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Invalid executable file."); + } + print_verbose(vformat("CodeSign: Signing executable for cputype: %d ...", mh.get_cputype())); + + print_verbose(vformat("CodeSign: Generating CodeDirectory...")); + Ref<CodeSignCodeDirectory> cd1 = memnew(CodeSignCodeDirectory(0x14, 0x01, true, uuid_str, team_id.utf8(), 12, mh.get_exe_limit(), mh.get_code_limit())); + Ref<CodeSignCodeDirectory> cd2 = memnew(CodeSignCodeDirectory(0x20, 0x02, true, uuid_str, team_id.utf8(), 12, mh.get_exe_limit(), mh.get_code_limit())); + print_verbose(vformat("CodeSign: Calculating special slot hashes...")); + if (info_hash2.size() == 0x20) { + cd2->set_hash_in_slot(info_hash2, CodeSignCodeDirectory::SLOT_INFO_PLIST); + } + if (info_hash1.size() == 0x14) { + cd1->set_hash_in_slot(info_hash1, CodeSignCodeDirectory::SLOT_INFO_PLIST); + } + cd1->set_hash_in_slot(rq->get_hash_sha1(), CodeSignCodeDirectory::Slot::SLOT_REQUIREMENTS); + cd2->set_hash_in_slot(rq->get_hash_sha256(), CodeSignCodeDirectory::Slot::SLOT_REQUIREMENTS); + if (res_hash2.size() == 0x20) { + cd2->set_hash_in_slot(res_hash2, CodeSignCodeDirectory::SLOT_RESOURCES); + } + if (res_hash1.size() == 0x14) { + cd1->set_hash_in_slot(res_hash1, CodeSignCodeDirectory::SLOT_RESOURCES); + } + if (cet.is_valid()) { + cd1->set_hash_in_slot(cet->get_hash_sha1(), CodeSignCodeDirectory::Slot::SLOT_ENTITLEMENTS); //Text variant. + cd2->set_hash_in_slot(cet->get_hash_sha256(), CodeSignCodeDirectory::Slot::SLOT_ENTITLEMENTS); + } + if (ceb.is_valid()) { + cd1->set_hash_in_slot(ceb->get_hash_sha1(), CodeSignCodeDirectory::Slot::SLOT_DER_ENTITLEMENTS); //ASN.1 variant. + cd2->set_hash_in_slot(ceb->get_hash_sha256(), CodeSignCodeDirectory::Slot::SLOT_DER_ENTITLEMENTS); + } + + // Calculate signature size. + int sign_size = 12; // SuperBlob header. + sign_size += cd1->get_size() + 8; + sign_size += cd2->get_size() + 8; + sign_size += rq->get_size() + 8; + if (cet.is_valid()) { + sign_size += cet->get_size() + 8; + } + if (ceb.is_valid()) { + sign_size += ceb->get_size() + 8; + } + sign_size += 16; // Empty signature size. + + // Alloc/resize signature load command. + print_verbose(vformat("CodeSign: Reallocating space for the signature superblob (%d)...", sign_size)); + if (!mh.set_signature_size(sign_size)) { + CLEANUP(); + r_error_msg = TTR("Can't resize signature load command."); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Can't resize signature load command."); + } + + print_verbose(vformat("CodeSign: Calculating executable code hashes...")); + // Calculate executable code hashes. + PackedByteArray buffer; + PackedByteArray hash1, hash2; + hash1.resize(0x14); + hash2.resize(0x20); + buffer.resize(1 << 12); + mh.get_file()->seek(0); + for (int32_t j = 0; j < cd2->get_page_count(); j++) { + mh.get_file()->get_buffer(buffer.ptrw(), (1 << 12)); + CryptoCore::SHA256Context ctx2; + ctx2.start(); + ctx2.update(buffer.ptr(), (1 << 12)); + ctx2.finish(hash2.ptrw()); + cd2->set_hash_in_slot(hash2, j); + + CryptoCore::SHA1Context ctx1; + ctx1.start(); + ctx1.update(buffer.ptr(), (1 << 12)); + ctx1.finish(hash1.ptrw()); + cd1->set_hash_in_slot(hash1, j); + } + if (cd2->get_page_remainder() > 0) { + mh.get_file()->get_buffer(buffer.ptrw(), cd2->get_page_remainder()); + CryptoCore::SHA256Context ctx2; + ctx2.start(); + ctx2.update(buffer.ptr(), cd2->get_page_remainder()); + ctx2.finish(hash2.ptrw()); + cd2->set_hash_in_slot(hash2, cd2->get_page_count()); + + CryptoCore::SHA1Context ctx1; + ctx1.start(); + ctx1.update(buffer.ptr(), cd1->get_page_remainder()); + ctx1.finish(hash1.ptrw()); + cd1->set_hash_in_slot(hash1, cd1->get_page_count()); + } + + print_verbose(vformat("CodeSign: Generating signature...")); + Ref<CodeSignSignature> cs; + cs = Ref<CodeSignSignature>(memnew(CodeSignSignature())); + + print_verbose(vformat("CodeSign: Writing signature superblob...")); + // Write signature data to the executable. + CodeSignSuperBlob sb = CodeSignSuperBlob(); + sb.add_blob(cd2); + sb.add_blob(cd1); + sb.add_blob(rq); + if (cet.is_valid()) { + sb.add_blob(cet); + } + if (ceb.is_valid()) { + sb.add_blob(ceb); + } + sb.add_blob(cs); + mh.get_file()->seek(mh.get_signature_offset()); + sb.write_to_file(mh.get_file()); + } + if (files_to_sign.size() > 1) { + print_verbose(vformat("CodeSign: Rebuilding fat executable...")); + LipO lip; + if (!lip.create_file(main_exe, files_to_sign)) { + CLEANUP(); + r_error_msg = TTR("Failed to create fat binary."); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Failed to create fat binary."); + } + CLEANUP(); + } + FileAccess::set_unix_permissions(main_exe, 0755); // Restore unix permissions. + return OK; +#undef CLEANUP +} + +Error CodeSign::codesign(bool p_use_hardened_runtime, bool p_force, const String &p_path, const String &p_ent_path, String &r_error_msg) { + DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (!da) { + r_error_msg = TTR("Can't get filesystem access."); + ERR_FAIL_V_MSG(ERR_CANT_CREATE, "CodeSign: Can't get filesystem access."); + } + + if (da->dir_exists(p_path)) { + String fmw_ver = "Current"; // Framework version (default). + String info_path; + String main_exe; + String bundle_path; + bool bundle = false; + bool ios_bundle = false; + if (da->file_exists(p_path.plus_file("Contents/Info.plist"))) { + info_path = p_path.plus_file("Contents/Info.plist"); + main_exe = p_path.plus_file("Contents/MacOS"); + bundle_path = p_path.plus_file("Contents"); + bundle = true; + } else if (da->file_exists(p_path.plus_file(vformat("Versions/%s/Resources/Info.plist", fmw_ver)))) { + info_path = p_path.plus_file(vformat("Versions/%s/Resources/Info.plist", fmw_ver)); + main_exe = p_path.plus_file(vformat("Versions/%s", fmw_ver)); + bundle_path = p_path.plus_file(vformat("Versions/%s", fmw_ver)); + bundle = true; + } else if (da->file_exists(p_path.plus_file("Info.plist"))) { + info_path = p_path.plus_file("Info.plist"); + main_exe = p_path; + bundle_path = p_path; + bundle = true; + ios_bundle = true; + } + if (bundle) { + return _codesign_file(p_use_hardened_runtime, p_force, info_path, main_exe, bundle_path, p_ent_path, ios_bundle, r_error_msg); + } else { + r_error_msg = TTR("Unknown bundle type."); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Unknown bundle type."); + } + } else if (da->file_exists(p_path)) { + return _codesign_file(p_use_hardened_runtime, p_force, "", p_path, "", p_ent_path, false, r_error_msg); + } else { + r_error_msg = TTR("Unknown object type."); + ERR_FAIL_V_MSG(FAILED, "CodeSign: Unknown object type."); + } +} + +#endif // MODULE_REGEX_ENABLED diff --git a/platform/osx/export/codesign.h b/platform/osx/export/codesign.h new file mode 100644 index 0000000000..927df79281 --- /dev/null +++ b/platform/osx/export/codesign.h @@ -0,0 +1,369 @@ +/*************************************************************************/ +/* codesign.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +// macOS code signature creation utility. +// +// Current implementation has the following limitation: +// - Only version 11.3.0 signatures are supported. +// - Only "framework" and "app" bundle types are supported. +// - Page hash array scattering is not supported. +// - Reading and writing binary property lists i snot supported (third-party frameworks with binary Info.plist will not work unless .plist is converted to text format). +// - Requirements code generator is not implemented (only hard-coded requirements for the ad-hoc signing is supported). +// - RFC5652/CMS blob generation is not implemented, supports ad-hoc signing only. + +#ifndef CODESIGN_H +#define CODESIGN_H + +#include "core/crypto/crypto.h" +#include "core/crypto/crypto_core.h" +#include "core/io/dir_access.h" +#include "core/io/file_access.h" +#include "core/object/ref_counted.h" + +#include "modules/modules_enabled.gen.h" // For regex. +#ifdef MODULE_REGEX_ENABLED +#include "modules/regex/regex.h" +#endif + +#include "plist.h" + +#ifdef MODULE_REGEX_ENABLED + +/*************************************************************************/ +/* CodeSignCodeResources */ +/*************************************************************************/ + +class CodeSignCodeResources { +public: + enum class CRMatch { + CR_MATCH_NO, + CR_MATCH_YES, + CR_MATCH_NESTED, + CR_MATCH_OPTIONAL, + }; + +private: + struct CRFile { + String name; + String hash; + String hash2; + bool optional; + bool nested; + String requirements; + }; + + struct CRRule { + String file_pattern; + String key; + int weight; + bool store; + CRRule() { + weight = 1; + store = true; + } + CRRule(const String &p_file_pattern, const String &p_key, int p_weight, bool p_store) { + file_pattern = p_file_pattern; + key = p_key; + weight = p_weight; + store = p_store; + } + }; + + Vector<CRRule> rules1; + Vector<CRRule> rules2; + + Vector<CRFile> files1; + Vector<CRFile> files2; + + String hash_sha1_base64(const String &p_path); + String hash_sha256_base64(const String &p_path); + +public: + void add_rule1(const String &p_rule, const String &p_key = "", int p_weight = 0, bool p_store = true); + void add_rule2(const String &p_rule, const String &p_key = "", int p_weight = 0, bool p_store = true); + + CRMatch match_rules1(const String &p_path) const; + CRMatch match_rules2(const String &p_path) const; + + bool add_file1(const String &p_root, const String &p_path); + bool add_file2(const String &p_root, const String &p_path); + bool add_nested_file(const String &p_root, const String &p_path, const String &p_exepath); + + bool add_folder_recursive(const String &p_root, const String &p_path = "", const String &p_main_exe_path = ""); + + bool save_to_file(const String &p_path); +}; + +/*************************************************************************/ +/* CodeSignBlob */ +/*************************************************************************/ + +class CodeSignBlob : public RefCounted { +public: + virtual PackedByteArray get_hash_sha1() const = 0; + virtual PackedByteArray get_hash_sha256() const = 0; + + virtual int get_size() const = 0; + virtual uint32_t get_index_type() const = 0; + + virtual void write_to_file(FileAccess *p_file) const = 0; +}; + +/*************************************************************************/ +/* CodeSignRequirements */ +/*************************************************************************/ + +// Note: Proper code generator is not implemented (any we probably won't ever need it), just a hardcoded bytecode for the limited set of cases. + +class CodeSignRequirements : public CodeSignBlob { + PackedByteArray blob; + + static inline size_t PAD(size_t s, size_t a) { + return (s % a == 0) ? 0 : (a - s % a); + } + + _FORCE_INLINE_ void _parse_certificate_slot(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const; + _FORCE_INLINE_ void _parse_key(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const; + _FORCE_INLINE_ void _parse_oid_key(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const; + _FORCE_INLINE_ void _parse_hash_string(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const; + _FORCE_INLINE_ void _parse_value(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const; + _FORCE_INLINE_ void _parse_date(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const; + _FORCE_INLINE_ bool _parse_match(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const; + +public: + CodeSignRequirements(); + CodeSignRequirements(const PackedByteArray &p_data); + + Vector<String> parse_requirements() const; + + virtual PackedByteArray get_hash_sha1() const override; + virtual PackedByteArray get_hash_sha256() const override; + + virtual int get_size() const override; + + virtual uint32_t get_index_type() const override { return 0x00000002; }; + virtual void write_to_file(FileAccess *p_file) const override; +}; + +/*************************************************************************/ +/* CodeSignEntitlementsText */ +/*************************************************************************/ + +// PList formatted entitlements. + +class CodeSignEntitlementsText : public CodeSignBlob { + PackedByteArray blob; + +public: + CodeSignEntitlementsText(); + CodeSignEntitlementsText(const String &p_string); + + virtual PackedByteArray get_hash_sha1() const override; + virtual PackedByteArray get_hash_sha256() const override; + + virtual int get_size() const override; + + virtual uint32_t get_index_type() const override { return 0x00000005; }; + virtual void write_to_file(FileAccess *p_file) const override; +}; + +/*************************************************************************/ +/* CodeSignEntitlementsBinary */ +/*************************************************************************/ + +// ASN.1 serialized entitlements. + +class CodeSignEntitlementsBinary : public CodeSignBlob { + PackedByteArray blob; + +public: + CodeSignEntitlementsBinary(); + CodeSignEntitlementsBinary(const String &p_string); + + virtual PackedByteArray get_hash_sha1() const override; + virtual PackedByteArray get_hash_sha256() const override; + + virtual int get_size() const override; + + virtual uint32_t get_index_type() const override { return 0x00000007; }; + virtual void write_to_file(FileAccess *p_file) const override; +}; + +/*************************************************************************/ +/* CodeSignCodeDirectory */ +/*************************************************************************/ + +// Code Directory, runtime options, code segment and special structure hashes. + +class CodeSignCodeDirectory : public CodeSignBlob { +public: + enum Slot { + SLOT_INFO_PLIST = -1, + SLOT_REQUIREMENTS = -2, + SLOT_RESOURCES = -3, + SLOT_APP_SPECIFIC = -4, // Unused. + SLOT_ENTITLEMENTS = -5, + SLOT_RESERVER1 = -6, // Unused. + SLOT_DER_ENTITLEMENTS = -7, + }; + + enum CodeSignExecSegFlags { + EXECSEG_MAIN_BINARY = 0x1, + EXECSEG_ALLOW_UNSIGNED = 0x10, + EXECSEG_DEBUGGER = 0x20, + EXECSEG_JIT = 0x40, + EXECSEG_SKIP_LV = 0x80, + EXECSEG_CAN_LOAD_CDHASH = 0x100, + EXECSEG_CAN_EXEC_CDHASH = 0x200, + }; + + enum CodeSignatureFlags { + SIGNATURE_HOST = 0x0001, + SIGNATURE_ADHOC = 0x0002, + SIGNATURE_TASK_ALLOW = 0x0004, + SIGNATURE_INSTALLER = 0x0008, + SIGNATURE_FORCED_LV = 0x0010, + SIGNATURE_INVALID_ALLOWED = 0x0020, + SIGNATURE_FORCE_HARD = 0x0100, + SIGNATURE_FORCE_KILL = 0x0200, + SIGNATURE_FORCE_EXPIRATION = 0x0400, + SIGNATURE_RESTRICT = 0x0800, + SIGNATURE_ENFORCEMENT = 0x1000, + SIGNATURE_LIBRARY_VALIDATION = 0x2000, + SIGNATURE_ENTITLEMENTS_VALIDATED = 0x4000, + SIGNATURE_NVRAM_UNRESTRICTED = 0x8000, + SIGNATURE_RUNTIME = 0x10000, + SIGNATURE_LINKER_SIGNED = 0x20000, + }; + +private: + PackedByteArray blob; + + struct CodeDirectoryHeader { + uint32_t version; // Using version 0x0020500. + uint32_t flags; // // Option flags. + uint32_t hash_offset; // Slot zero offset. + uint32_t ident_offset; // Identifier string offset. + uint32_t special_slots; // Nr. of slots with negative index. + uint32_t code_slots; // Nr. of slots with index >= 0, (code_limit / page_size). + uint32_t code_limit; // Everything before code signature load command offset. + uint8_t hash_size; // 20 (SHA-1) or 32 (SHA-256). + uint8_t hash_type; // 1 (SHA-1) or 2 (SHA-256). + uint8_t platform; // Not used. + uint8_t page_size; // Page size, power of two, 2^12 (4096). + uint32_t spare2; // Not used. + // Version 0x20100 + uint32_t scatter_vector_offset; // Set to 0 and ignore. + // Version 0x20200 + uint32_t team_offset; // Team id string offset. + // Version 0x20300 + uint32_t spare3; // Not used. + uint64_t code_limit_64; // Set to 0 and ignore. + // Version 0x20400 + uint64_t exec_seg_base; // Start of the signed code segmet. + uint64_t exec_seg_limit; // Code segment (__TEXT) vmsize. + uint64_t exec_seg_flags; // Executable segment flags. + // Version 0x20500 + uint32_t runtime; // Runtime version. + uint32_t pre_encrypt_offset; // Set to 0 and ignore. + }; + + int32_t pages = 0; + int32_t remain = 0; + int32_t code_slots = 0; + int32_t special_slots = 0; + +public: + CodeSignCodeDirectory(); + CodeSignCodeDirectory(uint8_t p_hash_size, uint8_t p_hash_type, bool p_main, const CharString &p_id, const CharString &p_team_id, uint32_t p_page_size, uint64_t p_exe_limit, uint64_t p_code_limit); + + int32_t get_page_count(); + int32_t get_page_remainder(); + + bool set_hash_in_slot(const PackedByteArray &p_hash, int p_slot); + + virtual PackedByteArray get_hash_sha1() const override; + virtual PackedByteArray get_hash_sha256() const override; + + virtual int get_size() const override; + virtual uint32_t get_index_type() const override { return 0x00000000; }; + + virtual void write_to_file(FileAccess *p_file) const override; +}; + +/*************************************************************************/ +/* CodeSignSignature */ +/*************************************************************************/ + +class CodeSignSignature : public CodeSignBlob { + PackedByteArray blob; + +public: + CodeSignSignature(); + + virtual PackedByteArray get_hash_sha1() const override; + virtual PackedByteArray get_hash_sha256() const override; + + virtual int get_size() const override; + virtual uint32_t get_index_type() const override { return 0x00010000; }; + + virtual void write_to_file(FileAccess *p_file) const override; +}; + +/*************************************************************************/ +/* CodeSignSuperBlob */ +/*************************************************************************/ + +class CodeSignSuperBlob { + Vector<Ref<CodeSignBlob>> blobs; + +public: + bool add_blob(const Ref<CodeSignBlob> &p_blob); + + int get_size() const; + void write_to_file(FileAccess *p_file) const; +}; + +/*************************************************************************/ +/* CodeSign */ +/*************************************************************************/ + +class CodeSign { + static PackedByteArray file_hash_sha1(const String &p_path); + static PackedByteArray file_hash_sha256(const String &p_path); + static Error _codesign_file(bool p_use_hardened_runtime, bool p_force, const String &p_info, const String &p_exe_path, const String &p_bundle_path, const String &p_ent_path, bool p_ios_bundle, String &r_error_msg); + +public: + static Error codesign(bool p_use_hardened_runtime, bool p_force, const String &p_path, const String &p_ent_path, String &r_error_msg); +}; + +#endif // MODULE_REGEX_ENABLED + +#endif // CODESIGN_H diff --git a/platform/osx/export/export.cpp b/platform/osx/export/export.cpp index 1164d76580..bd35b39e9e 100644 --- a/platform/osx/export/export.cpp +++ b/platform/osx/export/export.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -33,6 +33,9 @@ #include "export_plugin.h" void register_osx_exporter() { + EDITOR_DEF("export/macos/force_builtin_codesign", false); + EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::BOOL, "export/macos/force_builtin_codesign", PROPERTY_HINT_NONE)); + Ref<EditorExportPlatformOSX> platform; platform.instantiate(); diff --git a/platform/osx/export/export.h b/platform/osx/export/export.h index f8cf41c0e7..b386337a09 100644 --- a/platform/osx/export/export.h +++ b/platform/osx/export/export.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/platform/osx/export/export_plugin.cpp b/platform/osx/export/export_plugin.cpp index 8126510245..f0b58efb63 100644 --- a/platform/osx/export/export_plugin.cpp +++ b/platform/osx/export/export_plugin.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -28,6 +28,9 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ +#include "modules/modules_enabled.gen.h" // For regex. + +#include "codesign.h" #include "export_plugin.h" void EditorExportPlatformOSX::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) { @@ -44,6 +47,23 @@ void EditorExportPlatformOSX::get_preset_features(const Ref<EditorExportPreset> r_features->push_back("64"); } +bool EditorExportPlatformOSX::get_export_option_visibility(const String &p_option, const Map<StringName, Variant> &p_options) const { + // These options are not supported by built-in codesign, used on non macOS host. + if (!OS::get_singleton()->has_feature("macos")) { + if (p_option == "codesign/identity" || p_option == "codesign/timestamp" || p_option == "codesign/hardened_runtime" || p_option == "codesign/custom_options" || p_option.begins_with("notarization/")) { + return false; + } + } + + // These entitlements are required to run managed code, and are always enabled in Mono builds. + if (Engine::get_singleton()->has_singleton("GodotSharp")) { + if (p_option == "codesign/entitlements/allow_jit_code_execution" || p_option == "codesign/entitlements/allow_unsigned_executable_memory" || p_option == "codesign/entitlements/allow_dyld_environment_variables") { + return false; + } + } + return true; +} + void EditorExportPlatformOSX::get_export_options(List<ExportOption> *r_options) { r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); @@ -58,23 +78,28 @@ void EditorExportPlatformOSX::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version"), "1.0")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "display/high_res"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/microphone_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the microphone"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/location_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the location information"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/address_book_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the address book"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/calendar_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the calendar"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photos_library_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the photo library"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/desktop_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Desktop folder"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/documents_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Documents folder"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/downloads_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Downloads folder"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/network_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use network volumes"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/removable_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use removable volumes"), "")); -#ifdef OSX_ENABLED r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/enable"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/identity", PROPERTY_HINT_PLACEHOLDER_TEXT, "Type: Name (ID)"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/timestamp"), true)); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/hardened_runtime"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/replace_existing_signature"), true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/hardened_runtime"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/entitlements/custom_file", PROPERTY_HINT_GLOBAL_FILE, "*.plist"), "")); - if (!Engine::get_singleton()->has_singleton("GodotSharp")) { - // These entitlements are required to run managed code, and are always enabled in Mono builds. - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_jit_code_execution"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_unsigned_executable_memory"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_dyld_environment_variables"), false)); - } + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_jit_code_execution"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_unsigned_executable_memory"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_dyld_environment_variables"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/disable_library_validation"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/audio_input"), false)); @@ -103,7 +128,6 @@ void EditorExportPlatformOSX::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Apple ID email"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_password", PROPERTY_HINT_PLACEHOLDER_TEXT, "Enable two-factor authentication and provide app-specific password"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_team_id", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide team ID if your Apple ID belongs to multiple teams"), "")); -#endif r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "texture_format/s3tc"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "texture_format/etc"), false)); @@ -305,13 +329,56 @@ void EditorExportPlatformOSX::_fix_plist(const Ref<EditorExportPreset> &p_preset } else if (lines[i].find("$copyright") != -1) { strnew += lines[i].replace("$copyright", p_preset->get("application/copyright")) + "\n"; } else if (lines[i].find("$highres") != -1) { - strnew += lines[i].replace("$highres", p_preset->get("display/high_res") ? "<true/>" : "<false/>") + "\n"; - } else if (lines[i].find("$camera_usage_description") != -1) { - String description = p_preset->get("privacy/camera_usage_description"); - strnew += lines[i].replace("$camera_usage_description", description) + "\n"; - } else if (lines[i].find("$microphone_usage_description") != -1) { - String description = p_preset->get("privacy/microphone_usage_description"); - strnew += lines[i].replace("$microphone_usage_description", description) + "\n"; + strnew += lines[i].replace("$highres", p_preset->get("display/high_res") ? "\t<true/>" : "\t<false/>") + "\n"; + } else if (lines[i].find("$usage_descriptions") != -1) { + String descriptions; + if (!((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) { + descriptions += "\t<key>NSMicrophoneUsageDescription</key>\n"; + descriptions += "\t<string>" + (String)p_preset->get("privacy/microphone_usage_description") + "</string>\n"; + } + if (!((String)p_preset->get("privacy/camera_usage_description")).is_empty()) { + descriptions += "\t<key>NSCameraUsageDescription</key>\n"; + descriptions += "\t<string>" + (String)p_preset->get("privacy/camera_usage_description") + "</string>\n"; + } + if (!((String)p_preset->get("privacy/location_usage_description")).is_empty()) { + descriptions += "\t<key>NSLocationUsageDescription</key>\n"; + descriptions += "\t<string>" + (String)p_preset->get("privacy/location_usage_description") + "</string>\n"; + } + if (!((String)p_preset->get("privacy/address_book_usage_description")).is_empty()) { + descriptions += "\t<key>NSContactsUsageDescription</key>\n"; + descriptions += "\t<string>" + (String)p_preset->get("privacy/address_book_usage_description") + "</string>\n"; + } + if (!((String)p_preset->get("privacy/calendar_usage_description")).is_empty()) { + descriptions += "\t<key>NSCalendarsUsageDescription</key>\n"; + descriptions += "\t<string>" + (String)p_preset->get("privacy/calendar_usage_description") + "</string>\n"; + } + if (!((String)p_preset->get("privacy/photos_library_usage_description")).is_empty()) { + descriptions += "\t<key>NSPhotoLibraryUsageDescription</key>\n"; + descriptions += "\t<string>" + (String)p_preset->get("privacy/photos_library_usage_description") + "</string>\n"; + } + if (!((String)p_preset->get("privacy/desktop_folder_usage_description")).is_empty()) { + descriptions += "\t<key>NSDesktopFolderUsageDescription</key>\n"; + descriptions += "\t<string>" + (String)p_preset->get("privacy/desktop_folder_usage_description") + "</string>\n"; + } + if (!((String)p_preset->get("privacy/documents_folder_usage_description")).is_empty()) { + descriptions += "\t<key>NSDocumentsFolderUsageDescription</key>\n"; + descriptions += "\t<string>" + (String)p_preset->get("privacy/documents_folder_usage_description") + "</string>\n"; + } + if (!((String)p_preset->get("privacy/downloads_folder_usage_description")).is_empty()) { + descriptions += "\t<key>NSDownloadsFolderUsageDescription</key>\n"; + descriptions += "\t<string>" + (String)p_preset->get("privacy/downloads_folder_usage_description") + "</string>\n"; + } + if (!((String)p_preset->get("privacy/network_volumes_usage_description")).is_empty()) { + descriptions += "\t<key>NSNetworkVolumesUsageDescription</key>\n"; + descriptions += "\t<string>" + (String)p_preset->get("privacy/network_volumes_usage_description") + "</string>\n"; + } + if (!((String)p_preset->get("privacy/removable_volumes_usage_description")).is_empty()) { + descriptions += "\t<key>NSRemovableVolumesUsageDescription</key>\n"; + descriptions += "\t<string>" + (String)p_preset->get("privacy/removable_volumes_usage_description") + "</string>\n"; + } + if (!descriptions.is_empty()) { + strnew += lines[i].replace("$usage_descriptions", descriptions); + } } else { strnew += lines[i] + "\n"; } @@ -362,14 +429,16 @@ Error EditorExportPlatformOSX::_notarize(const Ref<EditorExportPreset> &p_preset Error err = OS::get_singleton()->execute("xcrun", args, &str, nullptr, true); ERR_FAIL_COND_V(err != OK, err); - print_line("altool (" + p_path + "):\n" + str); + print_verbose("altool (" + p_path + "):\n" + str); if (str.find("RequestUUID") == -1) { EditorNode::add_io_error("altool: " + str); return FAILED; } else { - print_line("Note: The notarization process generally takes less than an hour. When the process is completed, you'll receive an email."); - print_line(" You can check progress manually by opening a Terminal and running the following command:"); - print_line(" \"xcrun altool --notarization-history 0 -u <your email> -p <app-specific pwd>\""); + print_line(TTR("Note: The notarization process generally takes less than an hour. When the process is completed, you'll receive an email.")); + print_line(" " + TTR("You can check progress manually by opening a Terminal and running the following command:")); + print_line(" \"xcrun altool --notarization-history 0 -u <your email> -p <app-specific pwd>\""); + print_line(" " + TTR("Run the following command to staple notarization ticket to the exported application (optional):")); + print_line(" \"xcrun stapler staple <app path>\""); } #endif @@ -378,63 +447,191 @@ Error EditorExportPlatformOSX::_notarize(const Ref<EditorExportPreset> &p_preset } Error EditorExportPlatformOSX::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path) { + bool force_builtin_codesign = EditorSettings::get_singleton()->get("export/macos/force_builtin_codesign"); + bool ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-"); + + if ((!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) || force_builtin_codesign) { + print_verbose("using built-in codesign..."); +#ifdef MODULE_REGEX_ENABLED + #ifdef OSX_ENABLED - List<String> args; + if (p_preset->get("codesign/timestamp")) { + WARN_PRINT("Timestamping is not compatible with ad-hoc signature, and was disabled!"); + } + if (p_preset->get("codesign/hardened_runtime")) { + WARN_PRINT("Hardened Runtime is not compatible with ad-hoc signature, and was disabled!"); + } +#endif - if (p_preset->get("codesign/timestamp")) { - args.push_back("--timestamp"); - } - if (p_preset->get("codesign/hardened_runtime")) { - args.push_back("--options"); - args.push_back("runtime"); - } + String error_msg; + Error err = CodeSign::codesign(false, p_preset->get("codesign/replace_existing_signature"), p_path, p_ent_path, error_msg); + if (err != OK) { + EditorNode::add_io_error("Built-in CodeSign: " + error_msg); + return FAILED; + } +#else + ERR_FAIL_V_MSG(FAILED, "Built-in CodeSign require regex module"); +#endif + return OK; + } else { + print_verbose("using external codesign..."); + List<String> args; + if (p_preset->get("codesign/timestamp")) { + if (ad_hoc) { + WARN_PRINT("Timestamping is not compatible with ad-hoc signature, and was disabled!"); + } else { + args.push_back("--timestamp"); + } + } + if (p_preset->get("codesign/hardened_runtime")) { + if (ad_hoc) { + WARN_PRINT("Hardened Runtime is not compatible with ad-hoc signature, and was disabled!"); + } else { + args.push_back("--options"); + args.push_back("runtime"); + } + } - if (p_path.get_extension() != "dmg") { - args.push_back("--entitlements"); - args.push_back(p_ent_path); - } + if (p_path.get_extension() != "dmg") { + args.push_back("--entitlements"); + args.push_back(p_ent_path); + } + + PackedStringArray user_args = p_preset->get("codesign/custom_options"); + for (int i = 0; i < user_args.size(); i++) { + String user_arg = user_args[i].strip_edges(); + if (!user_arg.is_empty()) { + args.push_back(user_arg); + } + } + + args.push_back("-s"); + if (ad_hoc) { + args.push_back("-"); + } else { + args.push_back(p_preset->get("codesign/identity")); + } + + args.push_back("-v"); /* provide some more feedback */ + + if (p_preset->get("codesign/replace_existing_signature")) { + args.push_back("-f"); + } - PackedStringArray user_args = p_preset->get("codesign/custom_options"); - for (int i = 0; i < user_args.size(); i++) { - String user_arg = user_args[i].strip_edges(); - if (!user_arg.is_empty()) { - args.push_back(user_arg); + args.push_back(p_path); + + String str; + Error err = OS::get_singleton()->execute("codesign", args, &str, nullptr, true); + ERR_FAIL_COND_V(err != OK, err); + + print_verbose("codesign (" + p_path + "):\n" + str); + if (str.find("no identity found") != -1) { + EditorNode::add_io_error("CodeSign: " + TTR("No identity found.")); + return FAILED; + } + if ((str.find("unrecognized blob type") != -1) || (str.find("cannot read entitlement data") != -1)) { + EditorNode::add_io_error("CodeSign: " + TTR("Invalid entitlements file.")); + return FAILED; } + return OK; } +} - args.push_back("-s"); - if (p_preset->get("codesign/identity") == "") { - args.push_back("-"); - } else { - args.push_back(p_preset->get("codesign/identity")); +Error EditorExportPlatformOSX::_code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, + const String &p_ent_path, bool p_should_error_on_non_code) { +#ifdef OSX_ENABLED + static Vector<String> extensions_to_sign; + + if (extensions_to_sign.is_empty()) { + extensions_to_sign.push_back("dylib"); + extensions_to_sign.push_back("framework"); } - args.push_back("-v"); /* provide some more feedback */ + Error dir_access_error; + DirAccessRef dir_access{ DirAccess::open(p_path, &dir_access_error) }; - if (p_preset->get("codesign/replace_existing_signature")) { - args.push_back("-f"); + if (dir_access_error != OK) { + return dir_access_error; } - args.push_back(p_path); + dir_access->list_dir_begin(); + String current_file{ dir_access->get_next() }; + while (!current_file.is_empty()) { + String current_file_path{ p_path.plus_file(current_file) }; - String str; - Error err = OS::get_singleton()->execute("codesign", args, &str, nullptr, true); - ERR_FAIL_COND_V(err != OK, err); + if (current_file == ".." || current_file == ".") { + current_file = dir_access->get_next(); + continue; + } - print_line("codesign (" + p_path + "):\n" + str); - if (str.find("no identity found") != -1) { - EditorNode::add_io_error("codesign: no identity found"); - return FAILED; - } - if ((str.find("unrecognized blob type") != -1) || (str.find("cannot read entitlement data") != -1)) { - EditorNode::add_io_error("codesign: invalid entitlements file"); - return FAILED; + if (extensions_to_sign.find(current_file.get_extension()) > -1) { + Error code_sign_error{ _code_sign(p_preset, current_file_path, p_ent_path) }; + if (code_sign_error != OK) { + return code_sign_error; + } + } else if (dir_access->current_is_dir()) { + Error code_sign_error{ _code_sign_directory(p_preset, current_file_path, p_ent_path, p_should_error_on_non_code) }; + if (code_sign_error != OK) { + return code_sign_error; + } + } else if (p_should_error_on_non_code) { + ERR_PRINT(vformat("Cannot sign file %s.", current_file)); + return Error::FAILED; + } + + current_file = dir_access->get_next(); } #endif return OK; } +Error EditorExportPlatformOSX::_copy_and_sign_files(DirAccessRef &dir_access, const String &p_src_path, + const String &p_in_app_path, bool p_sign_enabled, + const Ref<EditorExportPreset> &p_preset, const String &p_ent_path, + bool p_should_error_on_non_code_sign) { + Error err{ OK }; + if (dir_access->dir_exists(p_src_path)) { +#ifndef UNIX_ENABLED + WARN_PRINT("Relative symlinks are not supported, exported " + p_src_path.get_file() + " might be broken!"); +#endif + print_verbose("export framework: " + p_src_path + " -> " + p_in_app_path); + err = dir_access->make_dir_recursive(p_in_app_path); + if (err == OK) { + err = dir_access->copy_dir(p_src_path, p_in_app_path, -1, true); + } + } else { + print_verbose("export dylib: " + p_src_path + " -> " + p_in_app_path); + err = dir_access->copy(p_src_path, p_in_app_path); + } + if (err == OK && p_sign_enabled) { + if (dir_access->dir_exists(p_src_path) && p_src_path.get_extension().is_empty()) { + // If it is a directory, find and sign all dynamic libraries. + err = _code_sign_directory(p_preset, p_in_app_path, p_ent_path, p_should_error_on_non_code_sign); + } else { + err = _code_sign(p_preset, p_in_app_path, p_ent_path); + } + } + return err; +} + +Error EditorExportPlatformOSX::_export_osx_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin, + const String &p_app_path_name, DirAccessRef &dir_access, + bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, + const String &p_ent_path) { + Error error{ OK }; + const Vector<String> &osx_plugins{ p_editor_export_plugin->get_osx_plugin_files() }; + for (int i = 0; i < osx_plugins.size(); ++i) { + String src_path{ ProjectSettings::get_singleton()->globalize_path(osx_plugins[i]) }; + String path_in_app{ p_app_path_name + "/Contents/PlugIns/" + src_path.get_file() }; + error = _copy_and_sign_files(dir_access, src_path, path_in_app, p_sign_enabled, p_preset, p_ent_path, false); + if (error != OK) { + break; + } + } + return error; +} + Error EditorExportPlatformOSX::_create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name) { List<String> args; @@ -455,12 +652,12 @@ Error EditorExportPlatformOSX::_create_dmg(const String &p_dmg_path, const Strin Error err = OS::get_singleton()->execute("hdiutil", args, &str, nullptr, true); ERR_FAIL_COND_V(err != OK, err); - print_line("hdiutil returned: " + str); + print_verbose("hdiutil returned: " + str); if (str.find("create failed") != -1) { if (str.find("File exists") != -1) { - EditorNode::add_io_error("hdiutil: create failed - file exists"); + EditorNode::add_io_error("hdiutil: " + TTR("DMG creation failed, file already exists.")); } else { - EditorNode::add_io_error("hdiutil: create failed"); + EditorNode::add_io_error("hdiutil: " + TTR("DMG create failed.")); } return FAILED; } @@ -497,13 +694,13 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p FileAccess *src_f = nullptr; zlib_filefunc_def io = zipio_create_io_from_file(&src_f); - if (ep.step("Creating app", 0)) { + if (ep.step(TTR("Creating app bundle"), 0)) { return ERR_SKIP; } unzFile src_pkg_zip = unzOpen2(src_pkg_name.utf8().get_data(), &io); if (!src_pkg_zip) { - EditorNode::add_io_error("Could not find template app to export:\n" + src_pkg_name); + EditorNode::add_io_error(TTR("Could not find template app to export:") + "\n" + src_pkg_name); return ERR_FILE_NOT_FOUND; } @@ -522,12 +719,27 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p pkg_name = OS::get_singleton()->get_safe_dir_name(pkg_name); - String export_format = use_dmg() && p_path.ends_with("dmg") ? "dmg" : "zip"; + String export_format; + if (use_dmg() && p_path.ends_with("dmg")) { + export_format = "dmg"; + } else if (p_path.ends_with("zip")) { + export_format = "zip"; + } else if (p_path.ends_with("app")) { + export_format = "app"; + } else { + EditorNode::add_io_error("Invalid export format"); + return ERR_CANT_CREATE; + } // Create our application bundle. String tmp_app_dir_name = pkg_name + ".app"; - String tmp_app_path_name = EditorPaths::get_singleton()->get_cache_dir().plus_file(tmp_app_dir_name); - print_line("Exporting to " + tmp_app_path_name); + String tmp_app_path_name; + if (export_format == "app") { + tmp_app_path_name = p_path; + } else { + tmp_app_path_name = EditorPaths::get_singleton()->get_cache_dir().plus_file(tmp_app_dir_name); + } + print_verbose("Exporting to " + tmp_app_path_name); Error err = OK; @@ -536,16 +748,22 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p err = ERR_CANT_CREATE; } + if (DirAccess::exists(tmp_app_dir_name)) { + if (tmp_app_dir->change_dir(tmp_app_path_name) == OK) { + tmp_app_dir->erase_contents_recursive(); + } + } + Array helpers = p_preset->get("codesign/entitlements/app_sandbox/helper_executables"); // Create our folder structure. if (err == OK) { - print_line("Creating " + tmp_app_path_name + "/Contents/MacOS"); + print_verbose("Creating " + tmp_app_path_name + "/Contents/MacOS"); err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/MacOS"); } if (err == OK) { - print_line("Creating " + tmp_app_path_name + "/Contents/Frameworks"); + print_verbose("Creating " + tmp_app_path_name + "/Contents/Frameworks"); err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Frameworks"); } @@ -555,10 +773,28 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p } if (err == OK) { - print_line("Creating " + tmp_app_path_name + "/Contents/Resources"); + print_verbose("Creating " + tmp_app_path_name + "/Contents/Resources"); err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Resources"); } + Vector<String> translations = ProjectSettings::get_singleton()->get("internationalization/locale/translations"); + if (translations.size() > 0) { + { + String fname = tmp_app_path_name + "/Contents/Resources/en.lproj"; + tmp_app_dir->make_dir_recursive(fname); + FileAccessRef f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + } + + for (const String &E : translations) { + Ref<Translation> tr = ResourceLoader::load(E); + if (tr.is_valid()) { + String fname = tmp_app_path_name + "/Contents/Resources/" + tr->get_locale() + ".lproj"; + tmp_app_dir->make_dir_recursive(fname); + FileAccessRef f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + } + } + } + // Now process our template. bool found_binary = false; Vector<String> dylibs_found; @@ -571,7 +807,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p char fname[16384]; ret = unzGetCurrentFileInfo(src_pkg_zip, &info, fname, 16384, nullptr, 0, nullptr, 0); - String file = fname; + String file = String::utf8(fname); Vector<uint8_t> data; data.resize(info.uncompressed_size); @@ -584,6 +820,25 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p // Write. file = file.replace_first("osx_template.app/", ""); + if (((info.external_fa >> 16L) & 0120000) == 0120000) { +#ifndef UNIX_ENABLED + WARN_PRINT(vformat("Relative symlinks are not supported on this OS, exported project might be broken!")); +#endif + // Handle symlinks in the archive. + file = tmp_app_path_name.plus_file(file); + if (err == OK) { + err = tmp_app_dir->make_dir_recursive(file.get_base_dir()); + } + if (err == OK) { + String lnk_data = String::utf8((const char *)data.ptr(), data.size()); + err = tmp_app_dir->create_link(lnk_data, file); + print_verbose(vformat("ADDING SYMLINK %s => %s\n", file, lnk_data)); + } + + ret = unzGoToNextFile(src_pkg_zip); + continue; // next + } + if (file == "Contents/Info.plist") { _fix_plist(p_preset, data, pkg_name); } @@ -647,7 +902,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p dylibs_found.push_back(file); } - print_line("ADDING: " + file + " size: " + itos(data.size())); + print_verbose("ADDING: " + file + " size: " + itos(data.size())); // Write it into our application bundle. file = tmp_app_path_name.plus_file(file); @@ -677,12 +932,12 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p unzClose(src_pkg_zip); if (!found_binary) { - ERR_PRINT("Requested template binary '" + binary_to_use + "' not found. It might be missing from your template archive."); + ERR_PRINT(vformat("Requested template binary '%s' not found. It might be missing from your template archive.", binary_to_use)); err = ERR_FILE_NOT_FOUND; } if (err == OK) { - if (ep.step("Making PKG", 1)) { + if (ep.step(TTR("Making PKG"), 1)) { return ERR_SKIP; } @@ -861,25 +1116,37 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p } } + bool ad_hoc = true; + if (err == OK) { +#ifdef OSX_ENABLED + String sign_identity = p_preset->get("codesign/identity"); +#else + String sign_identity = "-"; +#endif + ad_hoc = (sign_identity == "" || sign_identity == "-"); + bool lib_validation = p_preset->get("codesign/entitlements/disable_library_validation"); + if ((!dylibs_found.is_empty() || !shared_objects.is_empty()) && sign_enabled && ad_hoc && !lib_validation) { + ERR_PRINT("Application with an ad-hoc signature require 'Disable Library Validation' entitlement to load dynamic libraries."); + err = ERR_CANT_CREATE; + } + } + if (err == OK) { DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); for (int i = 0; i < shared_objects.size(); i++) { String src_path = ProjectSettings::get_singleton()->globalize_path(shared_objects[i].path); - if (da->dir_exists(src_path)) { -#ifndef UNIX_ENABLED - WARN_PRINT("Relative symlinks are not supported, exported " + src_path.get_file() + " might be broken!"); -#endif - print_verbose("export framework: " + src_path + " -> " + tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file()); - err = da->make_dir_recursive(tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file()); - if (err == OK) { - err = da->copy_dir(src_path, tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file(), -1, true); - } - } else { - print_verbose("export dylib: " + src_path + " -> " + tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file()); - err = da->copy(src_path, tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file()); + String path_in_app{ tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file() }; + err = _copy_and_sign_files(da, src_path, path_in_app, sign_enabled, p_preset, ent_path, true); + if (err != OK) { + break; } - if (err == OK && sign_enabled) { - err = _code_sign(p_preset, tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file(), ent_path); + } + + Vector<Ref<EditorExportPlugin>> export_plugins{ EditorExport::get_singleton()->get_export_plugins() }; + for (int i = 0; i < export_plugins.size(); ++i) { + err = _export_osx_plugins_for(export_plugins[i], tmp_app_path_name, da, sign_enabled, p_preset, ent_path); + if (err != OK) { + break; } } } @@ -893,31 +1160,31 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p } if (err == OK && sign_enabled) { - if (ep.step("Code signing bundle", 2)) { + if (ep.step(TTR("Code signing bundle"), 2)) { return ERR_SKIP; } - err = _code_sign(p_preset, tmp_app_path_name + "/Contents/MacOS/" + pkg_name, ent_path); + err = _code_sign(p_preset, tmp_app_path_name, ent_path); } if (export_format == "dmg") { // Create a DMG. if (err == OK) { - if (ep.step("Making DMG", 3)) { + if (ep.step(TTR("Making DMG"), 3)) { return ERR_SKIP; } err = _create_dmg(p_path, pkg_name, tmp_app_path_name); } // Sign DMG. - if (err == OK && sign_enabled) { - if (ep.step("Code signing DMG", 3)) { + if (err == OK && sign_enabled && !ad_hoc) { + if (ep.step(TTR("Code signing DMG"), 3)) { return ERR_SKIP; } err = _code_sign(p_preset, p_path, ent_path); } - } else { + } else if (export_format == "zip") { // Create ZIP. if (err == OK) { - if (ep.step("Making ZIP", 3)) { + if (ep.step(TTR("Making ZIP"), 3)) { return ERR_SKIP; } if (FileAccess::exists(p_path)) { @@ -934,22 +1201,34 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p } } +#ifdef OSX_ENABLED bool noto_enabled = p_preset->get("notarization/enable"); if (err == OK && noto_enabled) { - if (ep.step("Sending archive for notarization", 4)) { - return ERR_SKIP; + if (export_format == "app") { + WARN_PRINT("Notarization require app to be archived first, select DMG or ZIP export format instead."); + } else { + if (ep.step(TTR("Sending archive for notarization"), 4)) { + return ERR_SKIP; + } + err = _notarize(p_preset, p_path); } - err = _notarize(p_preset, p_path); } +#endif // Clean up temporary entitlements files. DirAccess::remove_file_or_error(hlp_ent_path); - // Clean up temporary .app dir. - tmp_app_dir->change_dir(tmp_app_path_name); - tmp_app_dir->erase_contents_recursive(); - tmp_app_dir->change_dir(".."); - tmp_app_dir->remove(tmp_app_dir_name); + // Clean up temporary .app dir and generated entitlements. + if ((String)(p_preset->get("codesign/entitlements/custom_file")) == "") { + tmp_app_dir->remove(ent_path); + } + if (export_format != "app") { + if (tmp_app_dir->change_dir(tmp_app_path_name) == OK) { + tmp_app_dir->erase_contents_recursive(); + tmp_app_dir->change_dir(".."); + tmp_app_dir->remove(tmp_app_dir_name); + } + } } return err; @@ -1051,7 +1330,7 @@ void EditorExportPlatformOSX::_zip_folder_recursive(zipFile &p_zip, const String FileAccessRef fa = FileAccess::open(dir.plus_file(f), FileAccess::READ); if (!fa) { - ERR_FAIL_MSG("Can't open file to read from path '" + String(dir.plus_file(f)) + "'."); + ERR_FAIL_MSG(vformat("Can't open file to read from path \"%s\".", dir.plus_file(f))); } const int bufsize = 16384; uint8_t buf[bufsize]; @@ -1075,10 +1354,9 @@ bool EditorExportPlatformOSX::can_export(const Ref<EditorExportPreset> &p_preset String err; bool valid = false; - // Look for export templates (first official, and if defined custom templates). - - bool dvalid = exists_export_template("osx.zip", &err); - bool rvalid = dvalid; // Both in the same ZIP. + // Look for export templates (custom templates). + bool dvalid = false; + bool rvalid = false; if (p_preset->get("custom_template/debug") != "") { dvalid = FileAccess::exists(p_preset->get("custom_template/debug")); @@ -1093,6 +1371,12 @@ bool EditorExportPlatformOSX::can_export(const Ref<EditorExportPreset> &p_preset } } + // Look for export templates (official templates, check only is custom templates are not set). + if (!dvalid || !rvalid) { + dvalid = exists_export_template("osx.zip", &err); + rvalid = dvalid; // Both in the same ZIP. + } + valid = dvalid || rvalid; r_missing_templates = !valid; @@ -1104,15 +1388,33 @@ bool EditorExportPlatformOSX::can_export(const Ref<EditorExportPreset> &p_preset } bool sign_enabled = p_preset->get("codesign/enable"); + +#ifdef OSX_ENABLED bool noto_enabled = p_preset->get("notarization/enable"); + bool ad_hoc = ((p_preset->get("codesign/identity") == "") || (p_preset->get("codesign/identity") == "-")); + + if (!ad_hoc && (bool)EditorSettings::get_singleton()->get("export/macos/force_builtin_codesign")) { + err += TTR("Warning: Built-in \"codesign\" is selected in the Editor Settings. Code signing is limited to ad-hoc signature only.") + "\n"; + } + if (!ad_hoc && !FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) { + err += TTR("Warning: Xcode command line tools are not installed, using built-in \"codesign\". Code signing is limited to ad-hoc signature only.") + "\n"; + } + if (noto_enabled) { + if (ad_hoc) { + err += TTR("Notarization: Notarization with the ad-hoc signature is not supported.") + "\n"; + valid = false; + } if (!sign_enabled) { - err += TTR("Notarization: code signing required.") + "\n"; + err += TTR("Notarization: Code signing is required for notarization.") + "\n"; valid = false; } - bool hr_enabled = p_preset->get("codesign/hardened_runtime"); - if (!hr_enabled) { - err += TTR("Notarization: hardened runtime required.") + "\n"; + if (!(bool)p_preset->get("codesign/hardened_runtime")) { + err += TTR("Notarization: Hardened runtime is required for notarization.") + "\n"; + valid = false; + } + if (!(bool)p_preset->get("codesign/timestamp")) { + err += TTR("Notarization: Timestamping is required for notarization.") + "\n"; valid = false; } if (p_preset->get("notarization/apple_id_name") == "") { @@ -1123,6 +1425,51 @@ bool EditorExportPlatformOSX::can_export(const Ref<EditorExportPreset> &p_preset err += TTR("Notarization: Apple ID password not specified.") + "\n"; valid = false; } + } else { + err += TTR("Warning: Notarization is disabled. Exported project will be blocked by Gatekeeper, if it's downloaded from an unknown source.") + "\n"; + if (!sign_enabled) { + err += TTR("Code signing is disabled. Exported project will not run on Macs with enabled Gatekeeper and Apple Silicon powered Macs.") + "\n"; + } else { + if ((bool)p_preset->get("codesign/hardened_runtime") && ad_hoc) { + err += TTR("Hardened Runtime is not compatible with ad-hoc signature, and will be disabled!") + "\n"; + } + if ((bool)p_preset->get("codesign/timestamp") && ad_hoc) { + err += TTR("Timestamping is not compatible with ad-hoc signature, and will be disabled!") + "\n"; + } + } + } +#else + err += TTR("Warning: Notarization is not supported on this OS. Exported project will be blocked by Gatekeeper, if it's downloaded from an unknown source.") + "\n"; + if (!sign_enabled) { + err += TTR("Code signing is disabled. Exported project will not run on Macs with enabled Gatekeeper and Apple Silicon powered Macs.") + "\n"; + } +#endif + + if (sign_enabled) { + if ((bool)p_preset->get("codesign/entitlements/audio_input") && ((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) { + err += TTR("Privacy: Microphone access is enabled, but usage description is not specified.") + "\n"; + valid = false; + } + if ((bool)p_preset->get("codesign/entitlements/camera") && ((String)p_preset->get("privacy/camera_usage_description")).is_empty()) { + err += TTR("Privacy: Camera access is enabled, but usage description is not specified.") + "\n"; + valid = false; + } + if ((bool)p_preset->get("codesign/entitlements/location") && ((String)p_preset->get("privacy/location_usage_description")).is_empty()) { + err += TTR("Privacy: Location information access is enabled, but usage description is not specified.") + "\n"; + valid = false; + } + if ((bool)p_preset->get("codesign/entitlements/address_book") && ((String)p_preset->get("privacy/address_book_usage_description")).is_empty()) { + err += TTR("Privacy: Address book access is enabled, but usage description is not specified.") + "\n"; + valid = false; + } + if ((bool)p_preset->get("codesign/entitlements/calendars") && ((String)p_preset->get("privacy/calendar_usage_description")).is_empty()) { + err += TTR("Privacy: Calendar access is enabled, but usage description is not specified.") + "\n"; + valid = false; + } + if ((bool)p_preset->get("codesign/entitlements/photos_library") && ((String)p_preset->get("privacy/photos_library_usage_description")).is_empty()) { + err += TTR("Privacy: Photo library access is enabled, but usage description is not specified.") + "\n"; + valid = false; + } } if (!err.is_empty()) { diff --git a/platform/osx/export/export_plugin.h b/platform/osx/export/export_plugin.h index ca5086622e..931ce7e41a 100644 --- a/platform/osx/export/export_plugin.h +++ b/platform/osx/export/export_plugin.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -58,16 +58,23 @@ class EditorExportPlatformOSX : public EditorExportPlatform { Error _notarize(const Ref<EditorExportPreset> &p_preset, const String &p_path); Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path); + Error _code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_should_error_on_non_code = true); + Error _copy_and_sign_files(DirAccessRef &dir_access, const String &p_src_path, const String &p_in_app_path, + bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path, + bool p_should_error_on_non_code_sign); + Error _export_osx_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin, const String &p_app_path_name, + DirAccessRef &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, + const String &p_ent_path); Error _create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name); void _zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name); -#ifdef OSX_ENABLED bool use_codesign() const { return true; } +#ifdef OSX_ENABLED bool use_dmg() const { return true; } #else - bool use_codesign() const { return false; } bool use_dmg() const { return false; } #endif + bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const { String pname = p_package; @@ -80,7 +87,7 @@ class EditorExportPlatformOSX : public EditorExportPlatform { for (int i = 0; i < pname.length(); i++) { char32_t c = pname[i]; - if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.')) { + if (!(is_ascii_alphanumeric_char(c) || c == '-' || c == '.')) { if (r_error) { *r_error = vformat(TTR("The character '%s' is not allowed in Identifier."), String::chr(c)); } @@ -94,6 +101,7 @@ class EditorExportPlatformOSX : public EditorExportPlatform { protected: virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) override; virtual void get_export_options(List<ExportOption> *r_options) override; + virtual bool get_export_option_visibility(const String &p_option, const Map<StringName, Variant> &p_options) const override; public: virtual String get_name() const override { return "macOS"; } @@ -106,6 +114,7 @@ public: list.push_back("dmg"); } list.push_back("zip"); + list.push_back("app"); return list; } virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; diff --git a/platform/osx/export/lipo.cpp b/platform/osx/export/lipo.cpp new file mode 100644 index 0000000000..66d2ecdbcf --- /dev/null +++ b/platform/osx/export/lipo.cpp @@ -0,0 +1,243 @@ +/*************************************************************************/ +/* lipo.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "modules/modules_enabled.gen.h" // For regex. + +#include "lipo.h" + +#ifdef MODULE_REGEX_ENABLED + +bool LipO::is_lipo(const String &p_path) { + FileAccessRef fb = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(!fb, false, vformat("LipO: Can't open file: \"%s\".", p_path)); + uint32_t magic = fb->get_32(); + return (magic == 0xbebafeca || magic == 0xcafebabe || magic == 0xbfbafeca || magic == 0xcafebabf); +} + +bool LipO::create_file(const String &p_output_path, const PackedStringArray &p_files) { + close(); + + fa = FileAccess::open(p_output_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(!fa, false, vformat("LipO: Can't open file: \"%s\".", p_output_path)); + + uint64_t max_size = 0; + for (int i = 0; i < p_files.size(); i++) { + MachO mh; + if (!mh.open_file(p_files[i])) { + ERR_FAIL_V_MSG(false, vformat("LipO: Invalid MachO file: \"%s.\"", p_files[i])); + } + + FatArch arch; + arch.cputype = mh.get_cputype(); + arch.cpusubtype = mh.get_cpusubtype(); + arch.offset = 0; + arch.size = mh.get_size(); + arch.align = mh.get_align(); + max_size += arch.size; + + archs.push_back(arch); + + FileAccessRef fb = FileAccess::open(p_files[i], FileAccess::READ); + if (!fb) { + close(); + ERR_FAIL_V_MSG(false, vformat("LipO: Can't open file: \"%s.\"", p_files[i])); + } + } + + // Write header. + bool is_64 = (max_size >= std::numeric_limits<uint32_t>::max()); + if (is_64) { + fa->store_32(0xbfbafeca); + } else { + fa->store_32(0xbebafeca); + } + fa->store_32(BSWAP32(archs.size())); + uint64_t offset = archs.size() * (is_64 ? 32 : 20) + 8; + for (int i = 0; i < archs.size(); i++) { + archs.write[i].offset = offset + PAD(offset, uint64_t(1) << archs[i].align); + if (is_64) { + fa->store_32(BSWAP32(archs[i].cputype)); + fa->store_32(BSWAP32(archs[i].cpusubtype)); + fa->store_64(BSWAP64(archs[i].offset)); + fa->store_64(BSWAP64(archs[i].size)); + fa->store_32(BSWAP32(archs[i].align)); + fa->store_32(0); + } else { + fa->store_32(BSWAP32(archs[i].cputype)); + fa->store_32(BSWAP32(archs[i].cpusubtype)); + fa->store_32(BSWAP32(archs[i].offset)); + fa->store_32(BSWAP32(archs[i].size)); + fa->store_32(BSWAP32(archs[i].align)); + } + offset = archs[i].offset + archs[i].size; + } + + // Write files and padding. + for (int i = 0; i < archs.size(); i++) { + FileAccessRef fb = FileAccess::open(p_files[i], FileAccess::READ); + if (!fb) { + close(); + ERR_FAIL_V_MSG(false, vformat("LipO: Can't open file: \"%s.\"", p_files[i])); + } + uint64_t cur = fa->get_position(); + for (uint64_t j = cur; j < archs[i].offset; j++) { + fa->store_8(0); + } + int pages = archs[i].size / 4096; + int remain = archs[i].size % 4096; + unsigned char step[4096]; + for (int j = 0; j < pages; j++) { + uint64_t br = fb->get_buffer(step, 4096); + if (br > 0) { + fa->store_buffer(step, br); + } + } + uint64_t br = fb->get_buffer(step, remain); + if (br > 0) { + fa->store_buffer(step, br); + } + fb->close(); + } + return true; +} + +bool LipO::open_file(const String &p_path) { + close(); + + fa = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(!fa, false, vformat("LipO: Can't open file: \"%s\".", p_path)); + + uint32_t magic = fa->get_32(); + if (magic == 0xbebafeca) { + // 32-bit fat binary, bswap. + uint32_t nfat_arch = BSWAP32(fa->get_32()); + for (uint32_t i = 0; i < nfat_arch; i++) { + FatArch arch; + arch.cputype = BSWAP32(fa->get_32()); + arch.cpusubtype = BSWAP32(fa->get_32()); + arch.offset = BSWAP32(fa->get_32()); + arch.size = BSWAP32(fa->get_32()); + arch.align = BSWAP32(fa->get_32()); + + archs.push_back(arch); + } + } else if (magic == 0xcafebabe) { + // 32-bit fat binary. + uint32_t nfat_arch = fa->get_32(); + for (uint32_t i = 0; i < nfat_arch; i++) { + FatArch arch; + arch.cputype = fa->get_32(); + arch.cpusubtype = fa->get_32(); + arch.offset = fa->get_32(); + arch.size = fa->get_32(); + arch.align = fa->get_32(); + + archs.push_back(arch); + } + } else if (magic == 0xbfbafeca) { + // 64-bit fat binary, bswap. + uint32_t nfat_arch = BSWAP32(fa->get_32()); + for (uint32_t i = 0; i < nfat_arch; i++) { + FatArch arch; + arch.cputype = BSWAP32(fa->get_32()); + arch.cpusubtype = BSWAP32(fa->get_32()); + arch.offset = BSWAP64(fa->get_64()); + arch.size = BSWAP64(fa->get_64()); + arch.align = BSWAP32(fa->get_32()); + fa->get_32(); // Skip, reserved. + + archs.push_back(arch); + } + } else if (magic == 0xcafebabf) { + // 64-bit fat binary. + uint32_t nfat_arch = fa->get_32(); + for (uint32_t i = 0; i < nfat_arch; i++) { + FatArch arch; + arch.cputype = fa->get_32(); + arch.cpusubtype = fa->get_32(); + arch.offset = fa->get_64(); + arch.size = fa->get_64(); + arch.align = fa->get_32(); + fa->get_32(); // Skip, reserved. + + archs.push_back(arch); + } + } else { + close(); + ERR_FAIL_V_MSG(false, vformat("LipO: Invalid fat binary: \"%s\".", p_path)); + } + return true; +} + +int LipO::get_arch_count() const { + ERR_FAIL_COND_V_MSG(!fa, 0, "LipO: File not opened."); + return archs.size(); +} + +bool LipO::extract_arch(int p_index, const String &p_path) { + ERR_FAIL_COND_V_MSG(!fa, false, "LipO: File not opened."); + ERR_FAIL_INDEX_V(p_index, archs.size(), false); + + FileAccessRef fb = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(!fb, false, vformat("LipO: Can't open file: \"%s\".", p_path)); + + fa->seek(archs[p_index].offset); + + int pages = archs[p_index].size / 4096; + int remain = archs[p_index].size % 4096; + unsigned char step[4096]; + for (int i = 0; i < pages; i++) { + uint64_t br = fa->get_buffer(step, 4096); + if (br > 0) { + fb->store_buffer(step, br); + } + } + uint64_t br = fa->get_buffer(step, remain); + if (br > 0) { + fb->store_buffer(step, br); + } + fb->close(); + return true; +} + +void LipO::close() { + if (fa) { + fa->close(); + memdelete(fa); + fa = nullptr; + } + archs.clear(); +} + +LipO::~LipO() { + close(); +} + +#endif // MODULE_REGEX_ENABLED diff --git a/platform/osx/export/lipo.h b/platform/osx/export/lipo.h new file mode 100644 index 0000000000..68bbe42dd6 --- /dev/null +++ b/platform/osx/export/lipo.h @@ -0,0 +1,76 @@ +/*************************************************************************/ +/* lipo.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +// Universal / Universal 2 fat binary file creator and extractor. + +#ifndef LIPO_H +#define LIPO_H + +#include "core/io/file_access.h" +#include "core/object/ref_counted.h" +#include "modules/modules_enabled.gen.h" // For regex. + +#include "macho.h" + +#ifdef MODULE_REGEX_ENABLED + +class LipO : public RefCounted { + struct FatArch { + uint32_t cputype; + uint32_t cpusubtype; + uint64_t offset; + uint64_t size; + uint32_t align; + }; + + FileAccess *fa = nullptr; + Vector<FatArch> archs; + + static inline size_t PAD(size_t s, size_t a) { + return (a - s % a); + } + +public: + static bool is_lipo(const String &p_path); + + bool create_file(const String &p_output_path, const PackedStringArray &p_files); + + bool open_file(const String &p_path); + int get_arch_count() const; + bool extract_arch(int p_index, const String &p_path); + + void close(); + + ~LipO(); +}; + +#endif // MODULE_REGEX_ENABLED + +#endif // LIPO_H diff --git a/platform/osx/export/macho.cpp b/platform/osx/export/macho.cpp new file mode 100644 index 0000000000..08f2a855b0 --- /dev/null +++ b/platform/osx/export/macho.cpp @@ -0,0 +1,556 @@ +/*************************************************************************/ +/* macho.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "modules/modules_enabled.gen.h" // For regex. + +#include "macho.h" + +#ifdef MODULE_REGEX_ENABLED + +uint32_t MachO::seg_align(uint64_t p_vmaddr, uint32_t p_min, uint32_t p_max) { + uint32_t align = p_max; + if (p_vmaddr != 0) { + uint64_t seg_align = 1; + align = 0; + while ((seg_align & p_vmaddr) == 0) { + seg_align = seg_align << 1; + align++; + } + align = CLAMP(align, p_min, p_max); + } + return align; +} + +bool MachO::alloc_signature(uint64_t p_size) { + ERR_FAIL_COND_V_MSG(!fa, false, "MachO: File not opened."); + if (signature_offset != 0) { + // Nothing to do, already have signature load command. + return true; + } + if (lc_limit == 0 || lc_limit + 16 > exe_base) { + ERR_FAIL_V_MSG(false, "MachO: Can't allocate signature load command, please use \"codesign_allocate\" utility first."); + } else { + // Add signature load command. + signature_offset = lc_limit; + + fa->seek(lc_limit); + LoadCommandHeader lc; + lc.cmd = LC_CODE_SIGNATURE; + lc.cmdsize = 16; + if (swap) { + lc.cmdsize = BSWAP32(lc.cmdsize); + } + fa->store_buffer((const uint8_t *)&lc, sizeof(LoadCommandHeader)); + + uint32_t lc_offset = fa->get_length() + PAD(fa->get_length(), 16); + uint32_t lc_size = 0; + if (swap) { + lc_offset = BSWAP32(lc_offset); + lc_size = BSWAP32(lc_size); + } + fa->store_32(lc_offset); + fa->store_32(lc_size); + + // Write new command number. + fa->seek(0x10); + uint32_t ncmds = fa->get_32(); + uint32_t cmdssize = fa->get_32(); + if (swap) { + ncmds = BSWAP32(ncmds); + cmdssize = BSWAP32(cmdssize); + } + ncmds += 1; + cmdssize += 16; + if (swap) { + ncmds = BSWAP32(ncmds); + cmdssize = BSWAP32(cmdssize); + } + fa->seek(0x10); + fa->store_32(ncmds); + fa->store_32(cmdssize); + + lc_limit = lc_limit + sizeof(LoadCommandHeader) + 8; + + return true; + } +} + +bool MachO::is_macho(const String &p_path) { + FileAccessRef fb = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(!fb, false, vformat("MachO: Can't open file: \"%s\".", p_path)); + uint32_t magic = fb->get_32(); + return (magic == 0xcefaedfe || magic == 0xfeedface || magic == 0xcffaedfe || magic == 0xfeedfacf); +} + +bool MachO::open_file(const String &p_path) { + fa = FileAccess::open(p_path, FileAccess::READ_WRITE); + ERR_FAIL_COND_V_MSG(!fa, false, vformat("MachO: Can't open file: \"%s\".", p_path)); + uint32_t magic = fa->get_32(); + MachHeader mach_header; + + // Read MachO header. + swap = (magic == 0xcffaedfe || magic == 0xcefaedfe); + if (magic == 0xcefaedfe || magic == 0xfeedface) { + // Thin 32-bit binary. + fa->get_buffer((uint8_t *)&mach_header, sizeof(MachHeader)); + } else if (magic == 0xcffaedfe || magic == 0xfeedfacf) { + // Thin 64-bit binary. + fa->get_buffer((uint8_t *)&mach_header, sizeof(MachHeader)); + fa->get_32(); // Skip extra reserved field. + } else { + ERR_FAIL_V_MSG(false, vformat("MachO: File is not a valid MachO binary: \"%s\".", p_path)); + } + + if (swap) { + mach_header.ncmds = BSWAP32(mach_header.ncmds); + mach_header.cpusubtype = BSWAP32(mach_header.cpusubtype); + mach_header.cputype = BSWAP32(mach_header.cputype); + } + cpusubtype = mach_header.cpusubtype; + cputype = mach_header.cputype; + align = 0; + exe_base = std::numeric_limits<uint64_t>::max(); + exe_limit = 0; + lc_limit = 0; + link_edit_offset = 0; + signature_offset = 0; + + // Read load commands. + for (uint32_t i = 0; i < mach_header.ncmds; i++) { + LoadCommandHeader lc; + fa->get_buffer((uint8_t *)&lc, sizeof(LoadCommandHeader)); + if (swap) { + lc.cmd = BSWAP32(lc.cmd); + lc.cmdsize = BSWAP32(lc.cmdsize); + } + uint64_t ps = fa->get_position(); + switch (lc.cmd) { + case LC_SEGMENT: { + LoadCommandSegment lc_seg; + fa->get_buffer((uint8_t *)&lc_seg, sizeof(LoadCommandSegment)); + if (swap) { + lc_seg.nsects = BSWAP32(lc_seg.nsects); + lc_seg.vmaddr = BSWAP32(lc_seg.vmaddr); + lc_seg.vmsize = BSWAP32(lc_seg.vmsize); + } + align = MAX(align, seg_align(lc_seg.vmaddr, 2, 15)); + if (String(lc_seg.segname) == "__TEXT") { + exe_limit = MAX(exe_limit, lc_seg.vmsize); + for (uint32_t j = 0; j < lc_seg.nsects; j++) { + Section lc_sect; + fa->get_buffer((uint8_t *)&lc_sect, sizeof(Section)); + if (String(lc_sect.sectname) == "__text") { + if (swap) { + exe_base = MIN(exe_base, BSWAP32(lc_sect.offset)); + } else { + exe_base = MIN(exe_base, lc_sect.offset); + } + } + if (swap) { + align = MAX(align, BSWAP32(lc_sect.align)); + } else { + align = MAX(align, lc_sect.align); + } + } + } else if (String(lc_seg.segname) == "__LINKEDIT") { + link_edit_offset = ps - 8; + } + } break; + case LC_SEGMENT_64: { + LoadCommandSegment64 lc_seg; + fa->get_buffer((uint8_t *)&lc_seg, sizeof(LoadCommandSegment64)); + if (swap) { + lc_seg.nsects = BSWAP32(lc_seg.nsects); + lc_seg.vmaddr = BSWAP64(lc_seg.vmaddr); + lc_seg.vmsize = BSWAP64(lc_seg.vmsize); + } + align = MAX(align, seg_align(lc_seg.vmaddr, 3, 15)); + if (String(lc_seg.segname) == "__TEXT") { + exe_limit = MAX(exe_limit, lc_seg.vmsize); + for (uint32_t j = 0; j < lc_seg.nsects; j++) { + Section64 lc_sect; + fa->get_buffer((uint8_t *)&lc_sect, sizeof(Section64)); + if (String(lc_sect.sectname) == "__text") { + if (swap) { + exe_base = MIN(exe_base, BSWAP32(lc_sect.offset)); + } else { + exe_base = MIN(exe_base, lc_sect.offset); + } + if (swap) { + align = MAX(align, BSWAP32(lc_sect.align)); + } else { + align = MAX(align, lc_sect.align); + } + } + } + } else if (String(lc_seg.segname) == "__LINKEDIT") { + link_edit_offset = ps - 8; + } + } break; + case LC_CODE_SIGNATURE: { + signature_offset = ps - 8; + } break; + default: { + } break; + } + fa->seek(ps + lc.cmdsize - 8); + lc_limit = ps + lc.cmdsize - 8; + } + + if (exe_limit == 0 || lc_limit == 0) { + ERR_FAIL_V_MSG(false, vformat("MachO: No load commands or executable code found: \"%s\".", p_path)); + } + + return true; +} + +uint64_t MachO::get_exe_base() { + ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + return exe_base; +} + +uint64_t MachO::get_exe_limit() { + ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + return exe_limit; +} + +int32_t MachO::get_align() { + ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + return align; +} + +uint32_t MachO::get_cputype() { + ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + return cputype; +} + +uint32_t MachO::get_cpusubtype() { + ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + return cpusubtype; +} + +uint64_t MachO::get_size() { + ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + return fa->get_length(); +} + +uint64_t MachO::get_signature_offset() { + ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(signature_offset == 0, 0, "MachO: No signature load command."); + + fa->seek(signature_offset + 8); + if (swap) { + return BSWAP32(fa->get_32()); + } else { + return fa->get_32(); + } +} + +uint64_t MachO::get_code_limit() { + ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + + if (signature_offset == 0) { + return fa->get_length() + PAD(fa->get_length(), 16); + } else { + return get_signature_offset(); + } +} + +uint64_t MachO::get_signature_size() { + ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(signature_offset == 0, 0, "MachO: No signature load command."); + + fa->seek(signature_offset + 12); + if (swap) { + return BSWAP32(fa->get_32()); + } else { + return fa->get_32(); + } +} + +bool MachO::is_signed() { + ERR_FAIL_COND_V_MSG(!fa, false, "MachO: File not opened."); + if (signature_offset == 0) { + return false; + } + + fa->seek(get_signature_offset()); + uint32_t magic = BSWAP32(fa->get_32()); + if (magic != 0xfade0cc0) { + return false; // No SuperBlob found. + } + fa->get_32(); // Skip size field, unused. + uint32_t count = BSWAP32(fa->get_32()); + for (uint32_t i = 0; i < count; i++) { + uint32_t index_type = BSWAP32(fa->get_32()); + uint32_t offset = BSWAP32(fa->get_32()); + if (index_type == 0x00000000) { // CodeDirectory index type. + fa->seek(get_signature_offset() + offset + 12); + uint32_t flags = BSWAP32(fa->get_32()); + if (flags & 0x20000) { + return false; // Found CD, linker-signed. + } else { + return true; // Found CD, not linker-signed. + } + } + } + return false; // No CD found. +} + +PackedByteArray MachO::get_cdhash_sha1() { + ERR_FAIL_COND_V_MSG(!fa, PackedByteArray(), "MachO: File not opened."); + if (signature_offset == 0) { + return PackedByteArray(); + } + + fa->seek(get_signature_offset()); + uint32_t magic = BSWAP32(fa->get_32()); + if (magic != 0xfade0cc0) { + return PackedByteArray(); // No SuperBlob found. + } + fa->get_32(); // Skip size field, unused. + uint32_t count = BSWAP32(fa->get_32()); + for (uint32_t i = 0; i < count; i++) { + fa->get_32(); // Index type, skip. + uint32_t offset = BSWAP32(fa->get_32()); + uint64_t pos = fa->get_position(); + + fa->seek(get_signature_offset() + offset); + uint32_t cdmagic = BSWAP32(fa->get_32()); + uint32_t cdsize = BSWAP32(fa->get_32()); + if (cdmagic == 0xfade0c02) { // CodeDirectory. + fa->seek(get_signature_offset() + offset + 36); + uint8_t hash_size = fa->get_8(); + uint8_t hash_type = fa->get_8(); + if (hash_size == 0x14 && hash_type == 0x01) { /* SHA-1 */ + PackedByteArray hash; + hash.resize(0x14); + + fa->seek(get_signature_offset() + offset); + PackedByteArray blob; + blob.resize(cdsize); + fa->get_buffer(blob.ptrw(), cdsize); + + CryptoCore::SHA1Context ctx; + ctx.start(); + ctx.update(blob.ptr(), blob.size()); + ctx.finish(hash.ptrw()); + + return hash; + } + } + fa->seek(pos); + } + return PackedByteArray(); +} + +PackedByteArray MachO::get_cdhash_sha256() { + ERR_FAIL_COND_V_MSG(!fa, PackedByteArray(), "MachO: File not opened."); + if (signature_offset == 0) { + return PackedByteArray(); + } + + fa->seek(get_signature_offset()); + uint32_t magic = BSWAP32(fa->get_32()); + if (magic != 0xfade0cc0) { + return PackedByteArray(); // No SuperBlob found. + } + fa->get_32(); // Skip size field, unused. + uint32_t count = BSWAP32(fa->get_32()); + for (uint32_t i = 0; i < count; i++) { + fa->get_32(); // Index type, skip. + uint32_t offset = BSWAP32(fa->get_32()); + uint64_t pos = fa->get_position(); + + fa->seek(get_signature_offset() + offset); + uint32_t cdmagic = BSWAP32(fa->get_32()); + uint32_t cdsize = BSWAP32(fa->get_32()); + if (cdmagic == 0xfade0c02) { // CodeDirectory. + fa->seek(get_signature_offset() + offset + 36); + uint8_t hash_size = fa->get_8(); + uint8_t hash_type = fa->get_8(); + if (hash_size == 0x20 && hash_type == 0x02) { /* SHA-256 */ + PackedByteArray hash; + hash.resize(0x20); + + fa->seek(get_signature_offset() + offset); + PackedByteArray blob; + blob.resize(cdsize); + fa->get_buffer(blob.ptrw(), cdsize); + + CryptoCore::SHA256Context ctx; + ctx.start(); + ctx.update(blob.ptr(), blob.size()); + ctx.finish(hash.ptrw()); + + return hash; + } + } + fa->seek(pos); + } + return PackedByteArray(); +} + +PackedByteArray MachO::get_requirements() { + ERR_FAIL_COND_V_MSG(!fa, PackedByteArray(), "MachO: File not opened."); + if (signature_offset == 0) { + return PackedByteArray(); + } + + fa->seek(get_signature_offset()); + uint32_t magic = BSWAP32(fa->get_32()); + if (magic != 0xfade0cc0) { + return PackedByteArray(); // No SuperBlob found. + } + fa->get_32(); // Skip size field, unused. + uint32_t count = BSWAP32(fa->get_32()); + for (uint32_t i = 0; i < count; i++) { + fa->get_32(); // Index type, skip. + uint32_t offset = BSWAP32(fa->get_32()); + uint64_t pos = fa->get_position(); + + fa->seek(get_signature_offset() + offset); + uint32_t rqmagic = BSWAP32(fa->get_32()); + uint32_t rqsize = BSWAP32(fa->get_32()); + if (rqmagic == 0xfade0c01) { // Requirements. + PackedByteArray blob; + fa->seek(get_signature_offset() + offset); + blob.resize(rqsize); + fa->get_buffer(blob.ptrw(), rqsize); + return blob; + } + fa->seek(pos); + } + return PackedByteArray(); +} + +const FileAccess *MachO::get_file() const { + return fa; +} + +FileAccess *MachO::get_file() { + return fa; +} + +bool MachO::set_signature_size(uint64_t p_size) { + ERR_FAIL_COND_V_MSG(!fa, false, "MachO: File not opened."); + + // Ensure signature load command exists. + ERR_FAIL_COND_V_MSG(link_edit_offset == 0, false, "MachO: No __LINKEDIT segment found."); + ERR_FAIL_COND_V_MSG(!alloc_signature(p_size), false, "MachO: Can't allocate signature load command."); + + // Update signature load command. + uint64_t old_size = get_signature_size(); + uint64_t new_size = p_size + PAD(p_size, 16384); + + if (new_size <= old_size) { + fa->seek(get_signature_offset()); + for (uint64_t i = 0; i < old_size; i++) { + fa->store_8(0x00); + } + return true; + } + + fa->seek(signature_offset + 12); + if (swap) { + fa->store_32(BSWAP32(new_size)); + } else { + fa->store_32(new_size); + } + + uint64_t end = get_signature_offset() + new_size; + + // Update "__LINKEDIT" segment. + LoadCommandHeader lc; + fa->seek(link_edit_offset); + fa->get_buffer((uint8_t *)&lc, sizeof(LoadCommandHeader)); + if (swap) { + lc.cmd = BSWAP32(lc.cmd); + lc.cmdsize = BSWAP32(lc.cmdsize); + } + switch (lc.cmd) { + case LC_SEGMENT: { + LoadCommandSegment lc_seg; + fa->get_buffer((uint8_t *)&lc_seg, sizeof(LoadCommandSegment)); + if (swap) { + lc_seg.vmsize = BSWAP32(lc_seg.vmsize); + lc_seg.filesize = BSWAP32(lc_seg.filesize); + lc_seg.fileoff = BSWAP32(lc_seg.fileoff); + } + + lc_seg.vmsize = end - lc_seg.fileoff; + lc_seg.vmsize += PAD(lc_seg.vmsize, 4096); + lc_seg.filesize = end - lc_seg.fileoff; + + if (swap) { + lc_seg.vmsize = BSWAP32(lc_seg.vmsize); + lc_seg.filesize = BSWAP32(lc_seg.filesize); + } + fa->seek(link_edit_offset + 8); + fa->store_buffer((const uint8_t *)&lc_seg, sizeof(LoadCommandSegment)); + } break; + case LC_SEGMENT_64: { + LoadCommandSegment64 lc_seg; + fa->get_buffer((uint8_t *)&lc_seg, sizeof(LoadCommandSegment64)); + if (swap) { + lc_seg.vmsize = BSWAP64(lc_seg.vmsize); + lc_seg.filesize = BSWAP64(lc_seg.filesize); + lc_seg.fileoff = BSWAP64(lc_seg.fileoff); + } + lc_seg.vmsize = end - lc_seg.fileoff; + lc_seg.vmsize += PAD(lc_seg.vmsize, 4096); + lc_seg.filesize = end - lc_seg.fileoff; + if (swap) { + lc_seg.vmsize = BSWAP64(lc_seg.vmsize); + lc_seg.filesize = BSWAP64(lc_seg.filesize); + } + fa->seek(link_edit_offset + 8); + fa->store_buffer((const uint8_t *)&lc_seg, sizeof(LoadCommandSegment64)); + } break; + default: { + ERR_FAIL_V_MSG(false, "MachO: Invalid __LINKEDIT segment type."); + } break; + } + fa->seek(get_signature_offset()); + for (uint64_t i = 0; i < new_size; i++) { + fa->store_8(0x00); + } + return true; +} + +MachO::~MachO() { + if (fa) { + fa->close(); + memdelete(fa); + fa = nullptr; + } +} + +#endif // MODULE_REGEX_ENABLED diff --git a/platform/osx/export/macho.h b/platform/osx/export/macho.h new file mode 100644 index 0000000000..e09906898b --- /dev/null +++ b/platform/osx/export/macho.h @@ -0,0 +1,217 @@ +/*************************************************************************/ +/* macho.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +// Mach-O binary object file format parser and editor. + +#ifndef MACHO_H +#define MACHO_H + +#include "core/crypto/crypto.h" +#include "core/crypto/crypto_core.h" +#include "core/io/file_access.h" +#include "core/object/ref_counted.h" +#include "modules/modules_enabled.gen.h" // For regex. + +#ifdef MODULE_REGEX_ENABLED + +class MachO : public RefCounted { + struct MachHeader { + uint32_t cputype; + uint32_t cpusubtype; + uint32_t filetype; + uint32_t ncmds; + uint32_t sizeofcmds; + uint32_t flags; + }; + + enum LoadCommandID { + LC_SEGMENT = 0x00000001, + LC_SYMTAB = 0x00000002, + LC_SYMSEG = 0x00000003, + LC_THREAD = 0x00000004, + LC_UNIXTHREAD = 0x00000005, + LC_LOADFVMLIB = 0x00000006, + LC_IDFVMLIB = 0x00000007, + LC_IDENT = 0x00000008, + LC_FVMFILE = 0x00000009, + LC_PREPAGE = 0x0000000a, + LC_DYSYMTAB = 0x0000000b, + LC_LOAD_DYLIB = 0x0000000c, + LC_ID_DYLIB = 0x0000000d, + LC_LOAD_DYLINKER = 0x0000000e, + LC_ID_DYLINKER = 0x0000000f, + LC_PREBOUND_DYLIB = 0x00000010, + LC_ROUTINES = 0x00000011, + LC_SUB_FRAMEWORK = 0x00000012, + LC_SUB_UMBRELLA = 0x00000013, + LC_SUB_CLIENT = 0x00000014, + LC_SUB_LIBRARY = 0x00000015, + LC_TWOLEVEL_HINTS = 0x00000016, + LC_PREBIND_CKSUM = 0x00000017, + LC_LOAD_WEAK_DYLIB = 0x80000018, + LC_SEGMENT_64 = 0x00000019, + LC_ROUTINES_64 = 0x0000001a, + LC_UUID = 0x0000001b, + LC_RPATH = 0x8000001c, + LC_CODE_SIGNATURE = 0x0000001d, + LC_SEGMENT_SPLIT_INFO = 0x0000001e, + LC_REEXPORT_DYLIB = 0x8000001f, + LC_LAZY_LOAD_DYLIB = 0x00000020, + LC_ENCRYPTION_INFO = 0x00000021, + LC_DYLD_INFO = 0x00000022, + LC_DYLD_INFO_ONLY = 0x80000022, + LC_LOAD_UPWARD_DYLIB = 0x80000023, + LC_VERSION_MIN_MACOSX = 0x00000024, + LC_VERSION_MIN_IPHONEOS = 0x00000025, + LC_FUNCTION_STARTS = 0x00000026, + LC_DYLD_ENVIRONMENT = 0x00000027, + LC_MAIN = 0x80000028, + LC_DATA_IN_CODE = 0x00000029, + LC_SOURCE_VERSION = 0x0000002a, + LC_DYLIB_CODE_SIGN_DRS = 0x0000002b, + LC_ENCRYPTION_INFO_64 = 0x0000002c, + LC_LINKER_OPTION = 0x0000002d, + LC_LINKER_OPTIMIZATION_HINT = 0x0000002e, + LC_VERSION_MIN_TVOS = 0x0000002f, + LC_VERSION_MIN_WATCHOS = 0x00000030, + }; + + struct LoadCommandHeader { + uint32_t cmd; + uint32_t cmdsize; + }; + + struct LoadCommandSegment { + char segname[16]; + uint32_t vmaddr; + uint32_t vmsize; + uint32_t fileoff; + uint32_t filesize; + uint32_t maxprot; + uint32_t initprot; + uint32_t nsects; + uint32_t flags; + }; + + struct LoadCommandSegment64 { + char segname[16]; + uint64_t vmaddr; + uint64_t vmsize; + uint64_t fileoff; + uint64_t filesize; + uint32_t maxprot; + uint32_t initprot; + uint32_t nsects; + uint32_t flags; + }; + + struct Section { + char sectname[16]; + char segname[16]; + uint32_t addr; + uint32_t size; + uint32_t offset; + uint32_t align; + uint32_t reloff; + uint32_t nreloc; + uint32_t flags; + uint32_t reserved1; + uint32_t reserved2; + }; + + struct Section64 { + char sectname[16]; + char segname[16]; + uint64_t addr; + uint64_t size; + uint32_t offset; + uint32_t align; + uint32_t reloff; + uint32_t nreloc; + uint32_t flags; + uint32_t reserved1; + uint32_t reserved2; + uint32_t reserved3; + }; + + FileAccess *fa = nullptr; + bool swap = false; + + uint64_t lc_limit = 0; + + uint64_t exe_limit = 0; + uint64_t exe_base = std::numeric_limits<uint64_t>::max(); // Start of first __text section. + uint32_t align = 0; + uint32_t cputype = 0; + uint32_t cpusubtype = 0; + + uint64_t link_edit_offset = 0; // __LINKEDIT segment offset. + uint64_t signature_offset = 0; // Load command offset. + + uint32_t seg_align(uint64_t p_vmaddr, uint32_t p_min, uint32_t p_max); + bool alloc_signature(uint64_t p_size); + + static inline size_t PAD(size_t s, size_t a) { + return (a - s % a); + } + +public: + static bool is_macho(const String &p_path); + + bool open_file(const String &p_path); + + uint64_t get_exe_base(); + uint64_t get_exe_limit(); + int32_t get_align(); + uint32_t get_cputype(); + uint32_t get_cpusubtype(); + uint64_t get_size(); + uint64_t get_code_limit(); + + uint64_t get_signature_offset(); + bool is_signed(); + + PackedByteArray get_cdhash_sha1(); + PackedByteArray get_cdhash_sha256(); + + PackedByteArray get_requirements(); + + const FileAccess *get_file() const; + FileAccess *get_file(); + + uint64_t get_signature_size(); + bool set_signature_size(uint64_t p_size); + + ~MachO(); +}; + +#endif // MODULE_REGEX_ENABLED + +#endif // MACHO_H diff --git a/platform/osx/export/plist.cpp b/platform/osx/export/plist.cpp new file mode 100644 index 0000000000..553b864180 --- /dev/null +++ b/platform/osx/export/plist.cpp @@ -0,0 +1,570 @@ +/*************************************************************************/ +/* plist.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "modules/modules_enabled.gen.h" // For regex. + +#include "plist.h" + +#ifdef MODULE_REGEX_ENABLED + +Ref<PListNode> PListNode::new_array() { + Ref<PListNode> node = memnew(PListNode()); + ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>()); + node->data_type = PList::PLNodeType::PL_NODE_TYPE_ARRAY; + return node; +} + +Ref<PListNode> PListNode::new_dict() { + Ref<PListNode> node = memnew(PListNode()); + ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>()); + node->data_type = PList::PLNodeType::PL_NODE_TYPE_DICT; + return node; +} + +Ref<PListNode> PListNode::new_string(const String &p_string) { + Ref<PListNode> node = memnew(PListNode()); + ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>()); + node->data_type = PList::PLNodeType::PL_NODE_TYPE_STRING; + node->data_string = p_string.utf8(); + return node; +} + +Ref<PListNode> PListNode::new_data(const String &p_string) { + Ref<PListNode> node = memnew(PListNode()); + ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>()); + node->data_type = PList::PLNodeType::PL_NODE_TYPE_DATA; + node->data_string = p_string.utf8(); + return node; +} + +Ref<PListNode> PListNode::new_date(const String &p_string) { + Ref<PListNode> node = memnew(PListNode()); + ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>()); + node->data_type = PList::PLNodeType::PL_NODE_TYPE_DATE; + node->data_string = p_string.utf8(); + return node; +} + +Ref<PListNode> PListNode::new_bool(bool p_bool) { + Ref<PListNode> node = memnew(PListNode()); + ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>()); + node->data_type = PList::PLNodeType::PL_NODE_TYPE_BOOLEAN; + node->data_bool = p_bool; + return node; +} + +Ref<PListNode> PListNode::new_int(int32_t p_int) { + Ref<PListNode> node = memnew(PListNode()); + ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>()); + node->data_type = PList::PLNodeType::PL_NODE_TYPE_INTEGER; + node->data_int = p_int; + return node; +} + +Ref<PListNode> PListNode::new_real(float p_real) { + Ref<PListNode> node = memnew(PListNode()); + ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>()); + node->data_type = PList::PLNodeType::PL_NODE_TYPE_REAL; + node->data_real = p_real; + return node; +} + +bool PListNode::push_subnode(const Ref<PListNode> &p_node, const String &p_key) { + ERR_FAIL_COND_V(p_node.is_null(), false); + if (data_type == PList::PLNodeType::PL_NODE_TYPE_DICT) { + ERR_FAIL_COND_V(p_key.is_empty(), false); + ERR_FAIL_COND_V(data_dict.has(p_key), false); + data_dict[p_key] = p_node; + return true; + } else if (data_type == PList::PLNodeType::PL_NODE_TYPE_ARRAY) { + data_array.push_back(p_node); + return true; + } else { + ERR_FAIL_V_MSG(false, "PList: Invalid parent node type, should be DICT or ARRAY."); + } +} + +size_t PListNode::get_asn1_size(uint8_t p_len_octets) const { + // Get size of all data, excluding type and size information. + switch (data_type) { + case PList::PLNodeType::PL_NODE_TYPE_NIL: { + return 0; + } break; + case PList::PLNodeType::PL_NODE_TYPE_DATA: + case PList::PLNodeType::PL_NODE_TYPE_DATE: { + ERR_FAIL_V_MSG(0, "PList: DATE and DATA nodes are not supported by ASN.1 serialization."); + } break; + case PList::PLNodeType::PL_NODE_TYPE_STRING: { + return data_string.length(); + } break; + case PList::PLNodeType::PL_NODE_TYPE_BOOLEAN: { + return 1; + } break; + case PList::PLNodeType::PL_NODE_TYPE_INTEGER: + case PList::PLNodeType::PL_NODE_TYPE_REAL: { + return 4; + } break; + case PList::PLNodeType::PL_NODE_TYPE_ARRAY: { + size_t size = 0; + for (int i = 0; i < data_array.size(); i++) { + size += 1 + _asn1_size_len(p_len_octets) + data_array[i]->get_asn1_size(p_len_octets); + } + return size; + } break; + case PList::PLNodeType::PL_NODE_TYPE_DICT: { + size_t size = 0; + for (const Map<String, Ref<PListNode>>::Element *it = data_dict.front(); it; it = it->next()) { + size += 1 + _asn1_size_len(p_len_octets); // Sequence. + size += 1 + _asn1_size_len(p_len_octets) + it->key().utf8().length(); //Key. + size += 1 + _asn1_size_len(p_len_octets) + it->value()->get_asn1_size(p_len_octets); // Value. + } + return size; + } break; + default: { + return 0; + } break; + } +} + +int PListNode::_asn1_size_len(uint8_t p_len_octets) { + if (p_len_octets > 1) { + return p_len_octets + 1; + } else { + return 1; + } +} + +void PListNode::store_asn1_size(PackedByteArray &p_stream, uint8_t p_len_octets) const { + uint32_t size = get_asn1_size(p_len_octets); + if (p_len_octets > 1) { + p_stream.push_back(0x80 + p_len_octets); + } + for (int i = p_len_octets - 1; i >= 0; i--) { + uint8_t x = (size >> i * 8) & 0xFF; + p_stream.push_back(x); + } +} + +bool PListNode::store_asn1(PackedByteArray &p_stream, uint8_t p_len_octets) const { + // Convert to binary ASN1 stream. + bool valid = true; + switch (data_type) { + case PList::PLNodeType::PL_NODE_TYPE_NIL: { + // Nothing to store. + } break; + case PList::PLNodeType::PL_NODE_TYPE_DATE: + case PList::PLNodeType::PL_NODE_TYPE_DATA: { + ERR_FAIL_V_MSG(false, "PList: DATE and DATA nodes are not supported by ASN.1 serialization."); + } break; + case PList::PLNodeType::PL_NODE_TYPE_STRING: { + p_stream.push_back(0x0C); + store_asn1_size(p_stream, p_len_octets); + for (int i = 0; i < data_string.size(); i++) { + p_stream.push_back(data_string[i]); + } + } break; + case PList::PLNodeType::PL_NODE_TYPE_BOOLEAN: { + p_stream.push_back(0x01); + store_asn1_size(p_stream, p_len_octets); + if (data_bool) { + p_stream.push_back(0x01); + } else { + p_stream.push_back(0x00); + } + } break; + case PList::PLNodeType::PL_NODE_TYPE_INTEGER: { + p_stream.push_back(0x02); + store_asn1_size(p_stream, p_len_octets); + for (int i = 4; i >= 0; i--) { + uint8_t x = (data_int >> i * 8) & 0xFF; + p_stream.push_back(x); + } + } break; + case PList::PLNodeType::PL_NODE_TYPE_REAL: { + p_stream.push_back(0x03); + store_asn1_size(p_stream, p_len_octets); + for (int i = 4; i >= 0; i--) { + uint8_t x = (data_int >> i * 8) & 0xFF; + p_stream.push_back(x); + } + } break; + case PList::PLNodeType::PL_NODE_TYPE_ARRAY: { + p_stream.push_back(0x30); // Sequence. + store_asn1_size(p_stream, p_len_octets); + for (int i = 0; i < data_array.size(); i++) { + valid = valid && data_array[i]->store_asn1(p_stream, p_len_octets); + } + } break; + case PList::PLNodeType::PL_NODE_TYPE_DICT: { + p_stream.push_back(0x31); // Set. + store_asn1_size(p_stream, p_len_octets); + for (const Map<String, Ref<PListNode>>::Element *it = data_dict.front(); it; it = it->next()) { + CharString cs = it->key().utf8(); + uint32_t size = cs.length(); + + // Sequence. + p_stream.push_back(0x30); + uint32_t seq_size = 2 * (1 + _asn1_size_len(p_len_octets)) + size + it->value()->get_asn1_size(p_len_octets); + if (p_len_octets > 1) { + p_stream.push_back(0x80 + p_len_octets); + } + for (int i = p_len_octets - 1; i >= 0; i--) { + uint8_t x = (seq_size >> i * 8) & 0xFF; + p_stream.push_back(x); + } + // Key. + p_stream.push_back(0x0C); + if (p_len_octets > 1) { + p_stream.push_back(0x80 + p_len_octets); + } + for (int i = p_len_octets - 1; i >= 0; i--) { + uint8_t x = (size >> i * 8) & 0xFF; + p_stream.push_back(x); + } + for (uint32_t i = 0; i < size; i++) { + p_stream.push_back(cs[i]); + } + // Value. + valid = valid && it->value()->store_asn1(p_stream, p_len_octets); + } + } break; + } + return valid; +} + +void PListNode::store_text(String &p_stream, uint8_t p_indent) const { + // Convert to text XML stream. + switch (data_type) { + case PList::PLNodeType::PL_NODE_TYPE_NIL: { + // Nothing to store. + } break; + case PList::PLNodeType::PL_NODE_TYPE_DATA: { + p_stream += String("\t").repeat(p_indent); + p_stream += "<data>\n"; + p_stream += String("\t").repeat(p_indent); + p_stream += data_string + "\n"; + p_stream += String("\t").repeat(p_indent); + p_stream += "</data>\n"; + } break; + case PList::PLNodeType::PL_NODE_TYPE_DATE: { + p_stream += String("\t").repeat(p_indent); + p_stream += "<date>"; + p_stream += data_string; + p_stream += "</date>\n"; + } break; + case PList::PLNodeType::PL_NODE_TYPE_STRING: { + p_stream += String("\t").repeat(p_indent); + p_stream += "<string>"; + p_stream += String::utf8(data_string); + p_stream += "</string>\n"; + } break; + case PList::PLNodeType::PL_NODE_TYPE_BOOLEAN: { + p_stream += String("\t").repeat(p_indent); + if (data_bool) { + p_stream += "<true/>\n"; + } else { + p_stream += "<false/>\n"; + } + } break; + case PList::PLNodeType::PL_NODE_TYPE_INTEGER: { + p_stream += String("\t").repeat(p_indent); + p_stream += "<integer>"; + p_stream += itos(data_int); + p_stream += "</integer>\n"; + } break; + case PList::PLNodeType::PL_NODE_TYPE_REAL: { + p_stream += String("\t").repeat(p_indent); + p_stream += "<real>"; + p_stream += rtos(data_real); + p_stream += "</real>\n"; + } break; + case PList::PLNodeType::PL_NODE_TYPE_ARRAY: { + p_stream += String("\t").repeat(p_indent); + p_stream += "<array>\n"; + for (int i = 0; i < data_array.size(); i++) { + data_array[i]->store_text(p_stream, p_indent + 1); + } + p_stream += String("\t").repeat(p_indent); + p_stream += "</array>\n"; + } break; + case PList::PLNodeType::PL_NODE_TYPE_DICT: { + p_stream += String("\t").repeat(p_indent); + p_stream += "<dict>\n"; + for (const Map<String, Ref<PListNode>>::Element *it = data_dict.front(); it; it = it->next()) { + p_stream += String("\t").repeat(p_indent + 1); + p_stream += "<key>"; + p_stream += it->key(); + p_stream += "</key>\n"; + it->value()->store_text(p_stream, p_indent + 1); + } + p_stream += String("\t").repeat(p_indent); + p_stream += "</dict>\n"; + } break; + } +} + +/*************************************************************************/ + +PList::PList() { + root = PListNode::new_dict(); +} + +PList::PList(const String &p_string) { + load_string(p_string); +} + +bool PList::load_file(const String &p_filename) { + root = Ref<PListNode>(); + + FileAccessRef fb = FileAccess::open(p_filename, FileAccess::READ); + if (!fb) { + return false; + } + + unsigned char magic[8]; + fb->get_buffer(magic, 8); + + if (String((const char *)magic, 8) == "bplist00") { + ERR_FAIL_V_MSG(false, "PList: Binary property lists are not supported."); + } else { + // Load text plist. + Error err; + Vector<uint8_t> array = FileAccess::get_file_as_array(p_filename, &err); + ERR_FAIL_COND_V(err != OK, false); + + String ret; + ret.parse_utf8((const char *)array.ptr(), array.size()); + return load_string(ret); + } +} + +bool PList::load_string(const String &p_string) { + root = Ref<PListNode>(); + + int pos = 0; + bool in_plist = false; + bool done_plist = false; + List<Ref<PListNode>> stack; + String key; + while (pos >= 0) { + int open_token_s = p_string.find("<", pos); + if (open_token_s == -1) { + ERR_FAIL_V_MSG(false, "PList: Unexpected end of data. No tags found."); + } + int open_token_e = p_string.find(">", open_token_s); + pos = open_token_e; + + String token = p_string.substr(open_token_s + 1, open_token_e - open_token_s - 1); + if (token.is_empty()) { + ERR_FAIL_V_MSG(false, "PList: Invalid token name."); + } + String value; + if (token[0] == '?' || token[0] == '!') { // Skip <?xml ... ?> and <!DOCTYPE ... > + int end_token_e = p_string.find(">", open_token_s); + pos = end_token_e; + continue; + } + + if (token.find("plist", 0) == 0) { + in_plist = true; + continue; + } + + if (token == "/plist") { + in_plist = false; + done_plist = true; + break; + } + + if (!in_plist) { + ERR_FAIL_V_MSG(false, "PList: Node outside of <plist> tag."); + } + + if (token == "dict") { + if (!stack.is_empty()) { + // Add subnode end enter it. + Ref<PListNode> dict = PListNode::new_dict(); + dict->data_type = PList::PLNodeType::PL_NODE_TYPE_DICT; + if (!stack.back()->get()->push_subnode(dict, key)) { + ERR_FAIL_V_MSG(false, "PList: Can't push subnode, invalid parent type."); + } + stack.push_back(dict); + } else { + // Add root node. + if (!root.is_null()) { + ERR_FAIL_V_MSG(false, "PList: Root node already set."); + } + Ref<PListNode> dict = PListNode::new_dict(); + stack.push_back(dict); + root = dict; + } + continue; + } + + if (token == "/dict") { + // Exit current dict. + if (stack.is_empty() || stack.back()->get()->data_type != PList::PLNodeType::PL_NODE_TYPE_DICT) { + ERR_FAIL_V_MSG(false, "PList: Mismatched </dict> tag."); + } + stack.pop_back(); + continue; + } + + if (token == "array") { + if (!stack.is_empty()) { + // Add subnode end enter it. + Ref<PListNode> arr = PListNode::new_array(); + if (!stack.back()->get()->push_subnode(arr, key)) { + ERR_FAIL_V_MSG(false, "PList: Can't push subnode, invalid parent type."); + } + stack.push_back(arr); + } else { + // Add root node. + if (!root.is_null()) { + ERR_FAIL_V_MSG(false, "PList: Root node already set."); + } + Ref<PListNode> arr = PListNode::new_array(); + stack.push_back(arr); + root = arr; + } + continue; + } + + if (token == "/array") { + // Exit current array. + if (stack.is_empty() || stack.back()->get()->data_type != PList::PLNodeType::PL_NODE_TYPE_ARRAY) { + ERR_FAIL_V_MSG(false, "PList: Mismatched </array> tag."); + } + stack.pop_back(); + continue; + } + + if (token[token.length() - 1] == '/') { + token = token.substr(0, token.length() - 1); + } else { + int end_token_s = p_string.find("</", pos); + if (end_token_s == -1) { + ERR_FAIL_V_MSG(false, vformat("PList: Mismatched <%s> tag.", token)); + } + int end_token_e = p_string.find(">", end_token_s); + pos = end_token_e; + String end_token = p_string.substr(end_token_s + 2, end_token_e - end_token_s - 2); + if (end_token != token) { + ERR_FAIL_V_MSG(false, vformat("PList: Mismatched <%s> and <%s> token pair.", token, end_token)); + } + value = p_string.substr(open_token_e + 1, end_token_s - open_token_e - 1); + } + if (token == "key") { + key = value; + } else { + Ref<PListNode> var = nullptr; + if (token == "true") { + var = PListNode::new_bool(true); + } else if (token == "false") { + var = PListNode::new_bool(false); + } else if (token == "integer") { + var = PListNode::new_int(value.to_int()); + } else if (token == "real") { + var = PListNode::new_real(value.to_float()); + } else if (token == "string") { + var = PListNode::new_string(value); + } else if (token == "data") { + var = PListNode::new_data(value); + } else if (token == "date") { + var = PListNode::new_date(value); + } else { + ERR_FAIL_V_MSG(false, "PList: Invalid value type."); + } + if (stack.is_empty() || !stack.back()->get()->push_subnode(var, key)) { + ERR_FAIL_V_MSG(false, "PList: Can't push subnode, invalid parent type."); + } + } + } + if (!stack.is_empty() || !done_plist) { + ERR_FAIL_V_MSG(false, "PList: Unexpected end of data. Root node is not closed."); + } + return true; +} + +PackedByteArray PList::save_asn1() const { + if (root == nullptr) { + ERR_FAIL_V_MSG(PackedByteArray(), "PList: Invalid PList, no root node."); + } + size_t size = root->get_asn1_size(1); + uint8_t len_octets = 0; + if (size < 0x80) { + len_octets = 1; + } else { + size = root->get_asn1_size(2); + if (size < 0xFFFF) { + len_octets = 2; + } else { + size = root->get_asn1_size(3); + if (size < 0xFFFFFF) { + len_octets = 3; + } else { + size = root->get_asn1_size(4); + if (size < 0xFFFFFFFF) { + len_octets = 4; + } else { + ERR_FAIL_V_MSG(PackedByteArray(), "PList: Data is too big for ASN.1 serializer, should be < 4 GiB."); + } + } + } + } + + PackedByteArray ret; + if (!root->store_asn1(ret, len_octets)) { + ERR_FAIL_V_MSG(PackedByteArray(), "PList: ASN.1 serializer error."); + } + return ret; +} + +String PList::save_text() const { + if (root == nullptr) { + ERR_FAIL_V_MSG(String(), "PList: Invalid PList, no root node."); + } + + String ret; + ret += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; + ret += "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"; + ret += "<plist version=\"1.0\">\n"; + + root->store_text(ret, 0); + + ret += "</plist>\n\n"; + return ret; +} + +Ref<PListNode> PList::get_root() { + return root; +} + +#endif // MODULE_REGEX_ENABLED diff --git a/platform/osx/export/plist.h b/platform/osx/export/plist.h new file mode 100644 index 0000000000..fb4aaaa935 --- /dev/null +++ b/platform/osx/export/plist.h @@ -0,0 +1,116 @@ +/*************************************************************************/ +/* plist.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +// Property list file format (application/x-plist) parser, property list ASN-1 serialization. + +#ifndef PLIST_H +#define PLIST_H + +#include "core/crypto/crypto_core.h" +#include "core/io/file_access.h" +#include "modules/modules_enabled.gen.h" // For regex. + +#ifdef MODULE_REGEX_ENABLED + +class PListNode; + +class PList : public RefCounted { + friend class PListNode; + +public: + enum PLNodeType { + PL_NODE_TYPE_NIL, + PL_NODE_TYPE_STRING, + PL_NODE_TYPE_ARRAY, + PL_NODE_TYPE_DICT, + PL_NODE_TYPE_BOOLEAN, + PL_NODE_TYPE_INTEGER, + PL_NODE_TYPE_REAL, + PL_NODE_TYPE_DATA, + PL_NODE_TYPE_DATE, + }; + +private: + Ref<PListNode> root; + +public: + PList(); + PList(const String &p_string); + + bool load_file(const String &p_filename); + bool load_string(const String &p_string); + + PackedByteArray save_asn1() const; + String save_text() const; + + Ref<PListNode> get_root(); +}; + +/*************************************************************************/ + +class PListNode : public RefCounted { + static int _asn1_size_len(uint8_t p_len_octets); + +public: + PList::PLNodeType data_type = PList::PLNodeType::PL_NODE_TYPE_NIL; + + CharString data_string; + Vector<Ref<PListNode>> data_array; + Map<String, Ref<PListNode>> data_dict; + union { + int32_t data_int; + bool data_bool; + float data_real; + }; + + static Ref<PListNode> new_array(); + static Ref<PListNode> new_dict(); + static Ref<PListNode> new_string(const String &p_string); + static Ref<PListNode> new_data(const String &p_string); + static Ref<PListNode> new_date(const String &p_string); + static Ref<PListNode> new_bool(bool p_bool); + static Ref<PListNode> new_int(int32_t p_int); + static Ref<PListNode> new_real(float p_real); + + bool push_subnode(const Ref<PListNode> &p_node, const String &p_key = ""); + + size_t get_asn1_size(uint8_t p_len_octets) const; + + void store_asn1_size(PackedByteArray &p_stream, uint8_t p_len_octets) const; + bool store_asn1(PackedByteArray &p_stream, uint8_t p_len_octets) const; + void store_text(String &p_stream, uint8_t p_indent) const; + + PListNode() {} + ~PListNode() {} +}; + +#endif // MODULE_REGEX_ENABLED + +#endif // PLIST_H diff --git a/platform/osx/gl_manager_osx.h b/platform/osx/gl_manager_osx_legacy.h index f86bc805c1..b5a1b9dd98 100644 --- a/platform/osx/gl_manager_osx.h +++ b/platform/osx/gl_manager_osx_legacy.h @@ -1,12 +1,12 @@ /*************************************************************************/ -/* gl_manager_osx.h */ +/* gl_manager_osx_legacy.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -28,8 +28,8 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef GL_MANAGER_OSX_H -#define GL_MANAGER_OSX_H +#ifndef GL_MANAGER_OSX_LEGACY_H +#define GL_MANAGER_OSX_LEGACY_H #if defined(OSX_ENABLED) && defined(GLES3_ENABLED) @@ -50,29 +50,21 @@ public: private: struct GLWindow { - GLWindow() { in_use = false; } - bool in_use; + int width = 0; + int height = 0; - DisplayServer::WindowID window_id; - int width; - int height; - - id window_view; - NSOpenGLContext *context; + id window_view = nullptr; + NSOpenGLContext *context = nullptr; }; - LocalVector<GLWindow> _windows; - - NSOpenGLContext *_shared_context = nullptr; - GLWindow *_current_window; + Map<DisplayServer::WindowID, GLWindow> windows; - Error _create_context(GLWindow &win); - void _internal_set_current_window(GLWindow *p_win); + NSOpenGLContext *shared_context = nullptr; + DisplayServer::WindowID current_window = DisplayServer::INVALID_WINDOW_ID; - GLWindow &get_window(unsigned int id) { return _windows[id]; } - const GLWindow &get_window(unsigned int id) const { return _windows[id]; } + Error create_context(GLWindow &win); - bool use_vsync; + bool use_vsync = false; ContextType context_type; public: @@ -80,7 +72,6 @@ public: void window_destroy(DisplayServer::WindowID p_window_id); void window_resize(DisplayServer::WindowID p_window_id, int p_width, int p_height); - // get directly from the cached GLWindow int window_get_width(DisplayServer::WindowID p_window_id = 0); int window_get_height(DisplayServer::WindowID p_window_id = 0); @@ -91,6 +82,7 @@ public: void window_make_current(DisplayServer::WindowID p_window_id); void window_update(DisplayServer::WindowID p_window_id); + void window_set_per_pixel_transparency_enabled(DisplayServer::WindowID p_window_id, bool p_enabled); Error initialize(); @@ -101,6 +93,5 @@ public: ~GLManager_OSX(); }; -#endif // defined(OSX_ENABLED) && defined(GLES3_ENABLED) - -#endif // GL_MANAGER_OSX_H +#endif // OSX_ENABLED && GLES3_ENABLED +#endif // GL_MANAGER_OSX_LEGACY_H diff --git a/platform/osx/gl_manager_osx.mm b/platform/osx/gl_manager_osx_legacy.mm index 60e0706fc0..fbe64e32a3 100644 --- a/platform/osx/gl_manager_osx.mm +++ b/platform/osx/gl_manager_osx_legacy.mm @@ -1,12 +1,12 @@ /*************************************************************************/ -/* gl_manager_osx.mm */ +/* gl_manager_osx_legacy.mm */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -28,7 +28,7 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "gl_manager_osx.h" +#include "gl_manager_osx_legacy.h" #ifdef OSX_ENABLED #ifdef GLES3_ENABLED @@ -36,7 +36,7 @@ #include <stdio.h> #include <stdlib.h> -Error GLManager_OSX::_create_context(GLWindow &win) { +Error GLManager_OSX::create_context(GLWindow &win) { NSOpenGLPixelFormatAttribute attributes[] = { NSOpenGLPFADoubleBuffer, NSOpenGLPFAClosestPolicy, @@ -50,10 +50,10 @@ Error GLManager_OSX::_create_context(GLWindow &win) { NSOpenGLPixelFormat *pixel_format = [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes]; ERR_FAIL_COND_V(pixel_format == nil, ERR_CANT_CREATE); - win.context = [[NSOpenGLContext alloc] initWithFormat:pixel_format shareContext:_shared_context]; + win.context = [[NSOpenGLContext alloc] initWithFormat:pixel_format shareContext:shared_context]; ERR_FAIL_COND_V(win.context == nil, ERR_CANT_CREATE); - if (_shared_context == nullptr) { - _shared_context = win.context; + if (shared_context == nullptr) { + shared_context = win.context; } [win.context setView:win.window_view]; @@ -63,40 +63,27 @@ Error GLManager_OSX::_create_context(GLWindow &win) { } Error GLManager_OSX::window_create(DisplayServer::WindowID p_window_id, id p_view, int p_width, int p_height) { - if (p_window_id >= (int)_windows.size()) { - _windows.resize(p_window_id + 1); - } - - GLWindow &win = _windows[p_window_id]; - win.in_use = true; - win.window_id = p_window_id; + GLWindow win; win.width = p_width; win.height = p_height; win.window_view = p_view; - if (_create_context(win) != OK) { - _windows.remove_at(_windows.size() - 1); + if (create_context(win) != OK) { return FAILED; } - window_make_current(_windows.size() - 1); + windows[p_window_id] = win; + window_make_current(p_window_id); return OK; } -void GLManager_OSX::_internal_set_current_window(GLWindow *p_win) { - _current_window = p_win; -} - void GLManager_OSX::window_resize(DisplayServer::WindowID p_window_id, int p_width, int p_height) { - if (p_window_id == -1) { + if (!windows.has(p_window_id)) { return; } - GLWindow &win = _windows[p_window_id]; - if (!win.in_use) { - return; - } + GLWindow &win = windows[p_window_id]; win.width = p_width; win.height = p_height; @@ -116,24 +103,37 @@ void GLManager_OSX::window_resize(DisplayServer::WindowID p_window_id, int p_wid } int GLManager_OSX::window_get_width(DisplayServer::WindowID p_window_id) { - return get_window(p_window_id).width; + if (!windows.has(p_window_id)) { + return 0; + } + + GLWindow &win = windows[p_window_id]; + return win.width; } int GLManager_OSX::window_get_height(DisplayServer::WindowID p_window_id) { - return get_window(p_window_id).height; + if (!windows.has(p_window_id)) { + return 0; + } + + GLWindow &win = windows[p_window_id]; + return win.height; } void GLManager_OSX::window_destroy(DisplayServer::WindowID p_window_id) { - GLWindow &win = get_window(p_window_id); - win.in_use = false; + if (!windows.has(p_window_id)) { + return; + } - if (_current_window == &win) { - _current_window = nullptr; + if (current_window == p_window_id) { + current_window = DisplayServer::INVALID_WINDOW_ID; } + + windows.erase(p_window_id); } void GLManager_OSX::release_current() { - if (!_current_window) { + if (current_window == DisplayServer::INVALID_WINDOW_ID) { return; } @@ -141,63 +141,59 @@ void GLManager_OSX::release_current() { } void GLManager_OSX::window_make_current(DisplayServer::WindowID p_window_id) { - if (p_window_id == -1) { - return; - } - - GLWindow &win = _windows[p_window_id]; - if (!win.in_use) { + if (current_window == p_window_id) { return; } - - if (&win == _current_window) { + if (!windows.has(p_window_id)) { return; } + GLWindow &win = windows[p_window_id]; [win.context makeCurrentContext]; - _internal_set_current_window(&win); + current_window = p_window_id; } void GLManager_OSX::make_current() { - if (!_current_window) { + if (current_window == DisplayServer::INVALID_WINDOW_ID) { return; } - if (!_current_window->in_use) { - WARN_PRINT("current window not in use!"); + if (!windows.has(current_window)) { return; } - [_current_window->context makeCurrentContext]; + + GLWindow &win = windows[current_window]; + [win.context makeCurrentContext]; } void GLManager_OSX::swap_buffers() { - // NO NEED TO CALL SWAP BUFFERS for each window... - // see https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glXSwapBuffers.xml - - if (!_current_window) { - return; - } - if (!_current_window->in_use) { - WARN_PRINT("current window not in use!"); - return; + for (Map<DisplayServer::WindowID, GLWindow>::Element *E = windows.front(); E; E = E->next()) { + [E->get().context flushBuffer]; } - [_current_window->context flushBuffer]; } void GLManager_OSX::window_update(DisplayServer::WindowID p_window_id) { - if (p_window_id == -1) { + if (!windows.has(p_window_id)) { return; } - GLWindow &win = _windows[p_window_id]; - if (!win.in_use) { - return; - } + GLWindow &win = windows[p_window_id]; + [win.context update]; +} - if (&win == _current_window) { +void GLManager_OSX::window_set_per_pixel_transparency_enabled(DisplayServer::WindowID p_window_id, bool p_enabled) { + if (!windows.has(p_window_id)) { return; } + GLWindow &win = windows[p_window_id]; + if (p_enabled) { + GLint opacity = 0; + [win.context setValues:&opacity forParameter:NSOpenGLContextParameterSurfaceOpacity]; + } else { + GLint opacity = 1; + [win.context setValues:&opacity forParameter:NSOpenGLContextParameterSurfaceOpacity]; + } [win.context update]; } @@ -207,6 +203,7 @@ Error GLManager_OSX::initialize() { void GLManager_OSX::set_use_vsync(bool p_use) { use_vsync = p_use; + CGLContextObj ctx = CGLGetCurrentContext(); if (ctx) { GLint swapInterval = p_use ? 1 : 0; @@ -221,8 +218,6 @@ bool GLManager_OSX::is_using_vsync() const { GLManager_OSX::GLManager_OSX(ContextType p_context_type) { context_type = p_context_type; - use_vsync = false; - _current_window = nullptr; } GLManager_OSX::~GLManager_OSX() { diff --git a/platform/osx/godot_application.h b/platform/osx/godot_application.h new file mode 100644 index 0000000000..8d48a659f3 --- /dev/null +++ b/platform/osx/godot_application.h @@ -0,0 +1,42 @@ +/*************************************************************************/ +/* godot_application.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef GODOT_APPLICATION_H +#define GODOT_APPLICATION_H + +#include "core/os/os.h" + +#import <AppKit/AppKit.h> +#import <Foundation/Foundation.h> + +@interface GodotApplication : NSApplication +@end + +#endif // GODOT_APPLICATION_H diff --git a/platform/osx/godot_application.mm b/platform/osx/godot_application.mm new file mode 100644 index 0000000000..00a58700e8 --- /dev/null +++ b/platform/osx/godot_application.mm @@ -0,0 +1,53 @@ +/*************************************************************************/ +/* godot_application.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "godot_application.h" + +#include "display_server_osx.h" + +@implementation GodotApplication + +- (void)sendEvent:(NSEvent *)event { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (ds) { + ds->send_event(event); + } + + // From http://cocoadev.com/index.pl?GameKeyboardHandlingAlmost + // This works around an AppKit bug, where key up events while holding + // down the command key don't get sent to the key window. + if ([event type] == NSEventTypeKeyUp && ([event modifierFlags] & NSEventModifierFlagCommand)) { + [[self keyWindow] sendEvent:event]; + } else { + [super sendEvent:event]; + } +} + +@end diff --git a/platform/osx/godot_application_delegate.h b/platform/osx/godot_application_delegate.h new file mode 100644 index 0000000000..8eec762d8f --- /dev/null +++ b/platform/osx/godot_application_delegate.h @@ -0,0 +1,45 @@ +/*************************************************************************/ +/* godot_application_delegate.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef GODOT_APPLICATION_DELEGATE_H +#define GODOT_APPLICATION_DELEGATE_H + +#include "core/os/os.h" + +#import <AppKit/AppKit.h> +#import <Foundation/Foundation.h> + +@interface GodotApplicationDelegate : NSObject +- (void)forceUnbundledWindowActivationHackStep1; +- (void)forceUnbundledWindowActivationHackStep2; +- (void)forceUnbundledWindowActivationHackStep3; +@end + +#endif // GODOT_APPLICATION_DELEGATE_H diff --git a/platform/osx/godot_application_delegate.mm b/platform/osx/godot_application_delegate.mm new file mode 100644 index 0000000000..be284ba543 --- /dev/null +++ b/platform/osx/godot_application_delegate.mm @@ -0,0 +1,132 @@ +/*************************************************************************/ +/* godot_application_delegate.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "godot_application_delegate.h" + +#include "display_server_osx.h" +#include "os_osx.h" + +@implementation GodotApplicationDelegate + +- (void)forceUnbundledWindowActivationHackStep1 { + // Step 1: Switch focus to macOS SystemUIServer process. + // Required to perform step 2, TransformProcessType will fail if app is already the in focus. + for (NSRunningApplication *app in [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.systemuiserver"]) { + [app activateWithOptions:NSApplicationActivateIgnoringOtherApps]; + break; + } + [self performSelector:@selector(forceUnbundledWindowActivationHackStep2) + withObject:nil + afterDelay:0.02]; +} + +- (void)forceUnbundledWindowActivationHackStep2 { + // Step 2: Register app as foreground process. + ProcessSerialNumber psn = { 0, kCurrentProcess }; + (void)TransformProcessType(&psn, kProcessTransformToForegroundApplication); + [self performSelector:@selector(forceUnbundledWindowActivationHackStep3) withObject:nil afterDelay:0.02]; +} + +- (void)forceUnbundledWindowActivationHackStep3 { + // Step 3: Switch focus back to app window. + [[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps]; +} + +- (void)applicationDidFinishLaunching:(NSNotification *)notice { + NSString *nsappname = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"]; + if (nsappname == nil || isatty(STDOUT_FILENO) || isatty(STDIN_FILENO) || isatty(STDERR_FILENO)) { + // If the executable is started from terminal or is not bundled, macOS WindowServer won't register and activate app window correctly (menu and title bar are grayed out and input ignored). + [self performSelector:@selector(forceUnbundledWindowActivationHackStep1) withObject:nil afterDelay:0.02]; + } +} + +- (void)applicationDidResignActive:(NSNotification *)notification { + if (OS::get_singleton()->get_main_loop()) { + OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_OUT); + } +} + +- (void)applicationDidBecomeActive:(NSNotification *)notification { + if (OS::get_singleton()->get_main_loop()) { + OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_IN); + } +} + +- (void)globalMenuCallback:(id)sender { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (ds) { + return ds->menu_callback(sender); + } +} + +- (NSMenu *)applicationDockMenu:(NSApplication *)sender { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (ds) { + return ds->get_dock_menu(); + } else { + return nullptr; + } +} + +- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename { + // Note: may be called called before main loop init! + OS_OSX *os = (OS_OSX *)OS::get_singleton(); + if (os) { + os->set_open_with_filename(String::utf8([filename UTF8String])); + } + +#ifdef TOOLS_ENABLED + // Open new instance. + if (os && os->get_main_loop()) { + List<String> args; + args.push_back(os->get_open_with_filename()); + String exec = os->get_executable_path(); + os->create_process(exec, args); + } +#endif + return YES; +} + +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (ds) { + ds->send_window_event(ds->get_window(DisplayServerOSX::MAIN_WINDOW_ID), DisplayServerOSX::WINDOW_EVENT_CLOSE_REQUEST); + } + return NSTerminateCancel; +} + +- (void)showAbout:(id)sender { + OS_OSX *os = (OS_OSX *)OS::get_singleton(); + if (os && os->get_main_loop()) { + os->get_main_loop()->notification(MainLoop::NOTIFICATION_WM_ABOUT); + } +} + +@end diff --git a/platform/osx/godot_content_view.h b/platform/osx/godot_content_view.h new file mode 100644 index 0000000000..7942d716dc --- /dev/null +++ b/platform/osx/godot_content_view.h @@ -0,0 +1,65 @@ +/*************************************************************************/ +/* godot_content_view.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef GODOT_CONTENT_VIEW_H +#define GODOT_CONTENT_VIEW_H + +#include "servers/display_server.h" + +#import <AppKit/AppKit.h> +#import <Foundation/Foundation.h> + +#if defined(GLES3_ENABLED) +#import <AppKit/NSOpenGLView.h> +#define RootView NSOpenGLView +#else +#define RootView NSView +#endif + +#import <QuartzCore/CAMetalLayer.h> + +@interface GodotContentView : RootView <NSTextInputClient> { + DisplayServer::WindowID window_id; + NSTrackingArea *tracking_area; + NSMutableAttributedString *marked_text; + bool ime_input_event_in_progress; + bool mouse_down_control; + bool ignore_momentum_scroll; +} + +- (void)processScrollEvent:(NSEvent *)event button:(MouseButton)button factor:(double)factor; +- (void)processPanEvent:(NSEvent *)event dx:(double)dx dy:(double)dy; +- (void)processMouseEvent:(NSEvent *)event index:(MouseButton)index mask:(MouseButton)mask pressed:(bool)pressed; +- (void)setWindowID:(DisplayServer::WindowID)wid; +- (void)cancelComposition; + +@end + +#endif // GODOT_CONTENT_VIEW_H diff --git a/platform/osx/godot_content_view.mm b/platform/osx/godot_content_view.mm new file mode 100644 index 0000000000..4e831e1ccc --- /dev/null +++ b/platform/osx/godot_content_view.mm @@ -0,0 +1,760 @@ +/*************************************************************************/ +/* godot_content_view.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "godot_content_view.h" + +#include "display_server_osx.h" +#include "key_mapping_osx.h" + +@implementation GodotContentView + +- (id)init { + self = [super init]; + window_id = DisplayServer::INVALID_WINDOW_ID; + tracking_area = nil; + ime_input_event_in_progress = false; + mouse_down_control = false; + ignore_momentum_scroll = false; + [self updateTrackingAreas]; + + if (@available(macOS 10.13, *)) { + [self registerForDraggedTypes:[NSArray arrayWithObject:NSPasteboardTypeFileURL]]; +#if !defined(__aarch64__) // Do not build deprectead 10.13 code on ARM. + } else { + [self registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]]; +#endif + } + marked_text = [[NSMutableAttributedString alloc] init]; + return self; +} + +- (void)setWindowID:(DisplayServerOSX::WindowID)wid { + window_id = wid; +} + +// MARK: Backing Layer + +- (CALayer *)makeBackingLayer { + return [[CAMetalLayer class] layer]; +} + +- (void)updateLayer { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + ds->window_update(window_id); + [super updateLayer]; +} + +- (BOOL)wantsUpdateLayer { + return YES; +} + +- (BOOL)isOpaque { + return YES; +} + +// MARK: IME + +- (BOOL)hasMarkedText { + return (marked_text.length > 0); +} + +- (NSRange)markedRange { + return NSMakeRange(0, marked_text.length); +} + +- (NSRange)selectedRange { + static const NSRange kEmptyRange = { NSNotFound, 0 }; + return kEmptyRange; +} + +- (void)setMarkedText:(id)aString selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange { + if ([aString isKindOfClass:[NSAttributedString class]]) { + marked_text = [[NSMutableAttributedString alloc] initWithAttributedString:aString]; + } else { + marked_text = [[NSMutableAttributedString alloc] initWithString:aString]; + } + if (marked_text.length == 0) { + [self unmarkText]; + return; + } + + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + if (wd.im_active) { + ime_input_event_in_progress = true; + ds->update_im_text(Point2i(selectedRange.location, selectedRange.length), String::utf8([[marked_text mutableString] UTF8String])); + } +} + +- (void)unmarkText { + ime_input_event_in_progress = false; + [[marked_text mutableString] setString:@""]; + + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + if (wd.im_active) { + ds->update_im_text(Point2i(), String()); + } +} + +- (NSArray *)validAttributesForMarkedText { + return [NSArray array]; +} + +- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange { + return nil; +} + +- (NSUInteger)characterIndexForPoint:(NSPoint)aPoint { + return 0; +} + +- (NSRect)firstRectForCharacterRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return NSMakeRect(0, 0, 0, 0); + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + const NSRect content_rect = [wd.window_view frame]; + const float scale = ds->screen_get_max_scale(); + NSRect point_in_window_rect = NSMakeRect(wd.im_position.x / scale, content_rect.size.height - (wd.im_position.y / scale) - 1, 0, 0); + NSPoint point_on_screen = [wd.window_object convertRectToScreen:point_in_window_rect].origin; + + return NSMakeRect(point_on_screen.x, point_on_screen.y, 0, 0); +} + +- (void)cancelComposition { + [self unmarkText]; + [[NSTextInputContext currentInputContext] discardMarkedText]; +} + +- (void)insertText:(id)aString { + [self insertText:aString replacementRange:NSMakeRange(0, 0)]; +} + +- (void)insertText:(id)aString replacementRange:(NSRange)replacementRange { + NSEvent *event = [NSApp currentEvent]; + + NSString *characters; + if ([aString isKindOfClass:[NSAttributedString class]]) { + characters = [aString string]; + } else { + characters = (NSString *)aString; + } + + NSCharacterSet *ctrl_chars = [NSCharacterSet controlCharacterSet]; + NSCharacterSet *wsnl_chars = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + if ([characters rangeOfCharacterFromSet:ctrl_chars].length && [characters rangeOfCharacterFromSet:wsnl_chars].length == 0) { + [[NSTextInputContext currentInputContext] discardMarkedText]; + [self cancelComposition]; + return; + } + + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + [self cancelComposition]; + return; + } + + Char16String text; + text.resize([characters length] + 1); + [characters getCharacters:(unichar *)text.ptrw() range:NSMakeRange(0, [characters length])]; + + String u32text; + u32text.parse_utf16(text.ptr(), text.length()); + + for (int i = 0; i < u32text.length(); i++) { + const char32_t codepoint = u32text[i]; + if ((codepoint & 0xFF00) == 0xF700) { + continue; + } + + DisplayServerOSX::KeyEvent ke; + + ke.window_id = window_id; + ke.osx_state = [event modifierFlags]; + ke.pressed = true; + ke.echo = false; + ke.raw = false; // IME input event. + ke.keycode = Key::NONE; + ke.physical_keycode = Key::NONE; + ke.unicode = codepoint; + + ds->push_to_key_event_buffer(ke); + } + [self cancelComposition]; +} + +// MARK: Drag and drop + +- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender { + return NSDragOperationCopy; +} + +- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender { + return NSDragOperationCopy; +} + +- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return NO; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + if (!wd.drop_files_callback.is_null()) { + Vector<String> files; + NSPasteboard *pboard = [sender draggingPasteboard]; + + if (@available(macOS 10.13, *)) { + NSArray *items = pboard.pasteboardItems; + for (NSPasteboardItem *item in items) { + NSString *url = [item stringForType:NSPasteboardTypeFileURL]; + NSString *file = [NSURL URLWithString:url].path; + files.push_back(String::utf8([file UTF8String])); + } +#if !defined(__aarch64__) // Do not build deprectead 10.13 code on ARM. + } else { + NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType]; + for (NSString *file in filenames) { + files.push_back(String::utf8([file UTF8String])); + } +#endif + } + + Variant v = files; + Variant *vp = &v; + Variant ret; + Callable::CallError ce; + wd.drop_files_callback.call((const Variant **)&vp, 1, ret, ce); + } + + return NO; +} + +// MARK: Focus + +- (BOOL)canBecomeKeyView { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return YES; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + return !wd.no_focus; +} + +- (BOOL)acceptsFirstResponder { + return YES; +} + +// MARK: Mouse + +- (void)cursorUpdate:(NSEvent *)event { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds) { + return; + } + + ds->cursor_update_shape(); +} + +- (void)processMouseEvent:(NSEvent *)event index:(MouseButton)index mask:(MouseButton)mask pressed:(bool)pressed { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + MouseButton last_button_state = ds->mouse_get_button_state(); + + if (pressed) { + last_button_state |= mask; + } else { + last_button_state &= (MouseButton)~mask; + } + ds->mouse_set_button_state(last_button_state); + + Ref<InputEventMouseButton> mb; + mb.instantiate(); + mb->set_window_id(window_id); + ds->update_mouse_pos(wd, [event locationInWindow]); + ds->get_key_modifier_state([event modifierFlags], mb); + mb->set_button_index(index); + mb->set_pressed(pressed); + mb->set_position(wd.mouse_pos); + mb->set_global_position(wd.mouse_pos); + mb->set_button_mask(last_button_state); + if (index == MouseButton::LEFT && pressed) { + mb->set_double_click([event clickCount] == 2); + } + + Input::get_singleton()->parse_input_event(mb); +} + +- (void)mouseDown:(NSEvent *)event { + if (([event modifierFlags] & NSEventModifierFlagControl)) { + mouse_down_control = true; + [self processMouseEvent:event index:MouseButton::RIGHT mask:MouseButton::MASK_RIGHT pressed:true]; + } else { + mouse_down_control = false; + [self processMouseEvent:event index:MouseButton::LEFT mask:MouseButton::MASK_LEFT pressed:true]; + } +} + +- (void)mouseDragged:(NSEvent *)event { + [self mouseMoved:event]; +} + +- (void)mouseUp:(NSEvent *)event { + if (mouse_down_control) { + [self processMouseEvent:event index:MouseButton::RIGHT mask:MouseButton::MASK_RIGHT pressed:false]; + } else { + [self processMouseEvent:event index:MouseButton::LEFT mask:MouseButton::MASK_LEFT pressed:false]; + } +} + +- (void)mouseMoved:(NSEvent *)event { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + + NSPoint delta = NSMakePoint([event deltaX], [event deltaY]); + NSPoint mpos = [event locationInWindow]; + + if (ds->update_mouse_wrap(wd, delta, mpos, [event timestamp])) { + return; + } + + Ref<InputEventMouseMotion> mm; + mm.instantiate(); + + mm->set_window_id(window_id); + mm->set_button_mask(ds->mouse_get_button_state()); + ds->update_mouse_pos(wd, mpos); + mm->set_position(wd.mouse_pos); + mm->set_pressure([event pressure]); + if ([event subtype] == NSEventSubtypeTabletPoint) { + const NSPoint p = [event tilt]; + mm->set_tilt(Vector2(p.x, p.y)); + } + mm->set_global_position(wd.mouse_pos); + mm->set_velocity(Input::get_singleton()->get_last_mouse_velocity()); + const Vector2i relativeMotion = Vector2i(delta.x, delta.y) * ds->screen_get_max_scale(); + mm->set_relative(relativeMotion); + ds->get_key_modifier_state([event modifierFlags], mm); + + Input::get_singleton()->parse_input_event(mm); +} + +- (void)rightMouseDown:(NSEvent *)event { + [self processMouseEvent:event index:MouseButton::RIGHT mask:MouseButton::MASK_RIGHT pressed:true]; +} + +- (void)rightMouseDragged:(NSEvent *)event { + [self mouseMoved:event]; +} + +- (void)rightMouseUp:(NSEvent *)event { + [self processMouseEvent:event index:MouseButton::RIGHT mask:MouseButton::MASK_RIGHT pressed:false]; +} + +- (void)otherMouseDown:(NSEvent *)event { + if ((int)[event buttonNumber] == 2) { + [self processMouseEvent:event index:MouseButton::MIDDLE mask:MouseButton::MASK_MIDDLE pressed:true]; + } else if ((int)[event buttonNumber] == 3) { + [self processMouseEvent:event index:MouseButton::MB_XBUTTON1 mask:MouseButton::MASK_XBUTTON1 pressed:true]; + } else if ((int)[event buttonNumber] == 4) { + [self processMouseEvent:event index:MouseButton::MB_XBUTTON2 mask:MouseButton::MASK_XBUTTON2 pressed:true]; + } else { + return; + } +} + +- (void)otherMouseDragged:(NSEvent *)event { + [self mouseMoved:event]; +} + +- (void)otherMouseUp:(NSEvent *)event { + if ((int)[event buttonNumber] == 2) { + [self processMouseEvent:event index:MouseButton::MIDDLE mask:MouseButton::MASK_MIDDLE pressed:false]; + } else if ((int)[event buttonNumber] == 3) { + [self processMouseEvent:event index:MouseButton::MB_XBUTTON1 mask:MouseButton::MASK_XBUTTON1 pressed:false]; + } else if ((int)[event buttonNumber] == 4) { + [self processMouseEvent:event index:MouseButton::MB_XBUTTON2 mask:MouseButton::MASK_XBUTTON2 pressed:false]; + } else { + return; + } +} + +- (void)mouseExited:(NSEvent *)event { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + if (ds->mouse_get_mode() != DisplayServer::MOUSE_MODE_CAPTURED) { + ds->send_window_event(wd, DisplayServerOSX::WINDOW_EVENT_MOUSE_EXIT); + } +} + +- (void)mouseEntered:(NSEvent *)event { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + if (ds->mouse_get_mode() != DisplayServer::MOUSE_MODE_CAPTURED) { + ds->send_window_event(wd, DisplayServerOSX::WINDOW_EVENT_MOUSE_ENTER); + } + + ds->cursor_update_shape(); +} + +- (void)magnifyWithEvent:(NSEvent *)event { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + + Ref<InputEventMagnifyGesture> ev; + ev.instantiate(); + ev->set_window_id(window_id); + ds->get_key_modifier_state([event modifierFlags], ev); + ds->update_mouse_pos(wd, [event locationInWindow]); + ev->set_position(wd.mouse_pos); + ev->set_factor([event magnification] + 1.0); + + Input::get_singleton()->parse_input_event(ev); +} + +- (void)updateTrackingAreas { + if (tracking_area != nil) { + [self removeTrackingArea:tracking_area]; + } + + NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited | NSTrackingActiveInKeyWindow | NSTrackingCursorUpdate | NSTrackingInVisibleRect; + tracking_area = [[NSTrackingArea alloc] initWithRect:[self bounds] options:options owner:self userInfo:nil]; + + [self addTrackingArea:tracking_area]; + [super updateTrackingAreas]; +} + +// MARK: Keyboard + +- (void)keyDown:(NSEvent *)event { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + + ignore_momentum_scroll = true; + + // Ignore all input if IME input is in progress. + if (!ime_input_event_in_progress) { + NSString *characters = [event characters]; + NSUInteger length = [characters length]; + + if (!wd.im_active && length > 0 && keycode_has_unicode(KeyMappingOSX::remap_key([event keyCode], [event modifierFlags]))) { + // Fallback unicode character handler used if IME is not active. + Char16String text; + text.resize([characters length] + 1); + [characters getCharacters:(unichar *)text.ptrw() range:NSMakeRange(0, [characters length])]; + + String u32text; + u32text.parse_utf16(text.ptr(), text.length()); + + for (int i = 0; i < u32text.length(); i++) { + const char32_t codepoint = u32text[i]; + + DisplayServerOSX::KeyEvent ke; + + ke.window_id = window_id; + ke.osx_state = [event modifierFlags]; + ke.pressed = true; + ke.echo = [event isARepeat]; + ke.keycode = KeyMappingOSX::remap_key([event keyCode], [event modifierFlags]); + ke.physical_keycode = KeyMappingOSX::translate_key([event keyCode]); + ke.raw = true; + ke.unicode = codepoint; + + ds->push_to_key_event_buffer(ke); + } + } else { + DisplayServerOSX::KeyEvent ke; + + ke.window_id = window_id; + ke.osx_state = [event modifierFlags]; + ke.pressed = true; + ke.echo = [event isARepeat]; + ke.keycode = KeyMappingOSX::remap_key([event keyCode], [event modifierFlags]); + ke.physical_keycode = KeyMappingOSX::translate_key([event keyCode]); + ke.raw = false; + ke.unicode = 0; + + ds->push_to_key_event_buffer(ke); + } + } + + // Pass events to IME handler + if (wd.im_active) { + [self interpretKeyEvents:[NSArray arrayWithObject:event]]; + } +} + +- (void)flagsChanged:(NSEvent *)event { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + ignore_momentum_scroll = true; + + // Ignore all input if IME input is in progress + if (!ime_input_event_in_progress) { + DisplayServerOSX::KeyEvent ke; + + ke.window_id = window_id; + ke.echo = false; + ke.raw = true; + + int key = [event keyCode]; + int mod = [event modifierFlags]; + + if (key == 0x36 || key == 0x37) { + if (mod & NSEventModifierFlagCommand) { + mod &= ~NSEventModifierFlagCommand; + ke.pressed = true; + } else { + ke.pressed = false; + } + } else if (key == 0x38 || key == 0x3c) { + if (mod & NSEventModifierFlagShift) { + mod &= ~NSEventModifierFlagShift; + ke.pressed = true; + } else { + ke.pressed = false; + } + } else if (key == 0x3a || key == 0x3d) { + if (mod & NSEventModifierFlagOption) { + mod &= ~NSEventModifierFlagOption; + ke.pressed = true; + } else { + ke.pressed = false; + } + } else if (key == 0x3b || key == 0x3e) { + if (mod & NSEventModifierFlagControl) { + mod &= ~NSEventModifierFlagControl; + ke.pressed = true; + } else { + ke.pressed = false; + } + } else { + return; + } + + ke.osx_state = mod; + ke.keycode = KeyMappingOSX::remap_key(key, mod); + ke.physical_keycode = KeyMappingOSX::translate_key(key); + ke.unicode = 0; + + ds->push_to_key_event_buffer(ke); + } +} + +- (void)keyUp:(NSEvent *)event { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + + // Ignore all input if IME input is in progress. + if (!ime_input_event_in_progress) { + NSString *characters = [event characters]; + NSUInteger length = [characters length]; + + // Fallback unicode character handler used if IME is not active. + if (!wd.im_active && length > 0 && keycode_has_unicode(KeyMappingOSX::remap_key([event keyCode], [event modifierFlags]))) { + Char16String text; + text.resize([characters length] + 1); + [characters getCharacters:(unichar *)text.ptrw() range:NSMakeRange(0, [characters length])]; + + String u32text; + u32text.parse_utf16(text.ptr(), text.length()); + + for (int i = 0; i < u32text.length(); i++) { + const char32_t codepoint = u32text[i]; + DisplayServerOSX::KeyEvent ke; + + ke.window_id = window_id; + ke.osx_state = [event modifierFlags]; + ke.pressed = false; + ke.echo = [event isARepeat]; + ke.keycode = KeyMappingOSX::remap_key([event keyCode], [event modifierFlags]); + ke.physical_keycode = KeyMappingOSX::translate_key([event keyCode]); + ke.raw = true; + ke.unicode = codepoint; + + ds->push_to_key_event_buffer(ke); + } + } else { + DisplayServerOSX::KeyEvent ke; + + ke.window_id = window_id; + ke.osx_state = [event modifierFlags]; + ke.pressed = false; + ke.echo = [event isARepeat]; + ke.keycode = KeyMappingOSX::remap_key([event keyCode], [event modifierFlags]); + ke.physical_keycode = KeyMappingOSX::translate_key([event keyCode]); + ke.raw = true; + ke.unicode = 0; + + ds->push_to_key_event_buffer(ke); + } + } +} + +// MARK: Scroll and pan + +- (void)processScrollEvent:(NSEvent *)event button:(MouseButton)button factor:(double)factor { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + MouseButton mask = mouse_button_to_mask(button); + + Ref<InputEventMouseButton> sc; + sc.instantiate(); + + sc->set_window_id(window_id); + ds->get_key_modifier_state([event modifierFlags], sc); + sc->set_button_index(button); + sc->set_factor(factor); + sc->set_pressed(true); + sc->set_position(wd.mouse_pos); + sc->set_global_position(wd.mouse_pos); + MouseButton last_button_state = ds->mouse_get_button_state() | (MouseButton)mask; + sc->set_button_mask(last_button_state); + ds->mouse_set_button_state(last_button_state); + + Input::get_singleton()->parse_input_event(sc); + + sc.instantiate(); + sc->set_window_id(window_id); + sc->set_button_index(button); + sc->set_factor(factor); + sc->set_pressed(false); + sc->set_position(wd.mouse_pos); + sc->set_global_position(wd.mouse_pos); + last_button_state &= (MouseButton)~mask; + sc->set_button_mask(last_button_state); + ds->mouse_set_button_state(last_button_state); + + Input::get_singleton()->parse_input_event(sc); +} + +- (void)processPanEvent:(NSEvent *)event dx:(double)dx dy:(double)dy { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + + Ref<InputEventPanGesture> pg; + pg.instantiate(); + + pg->set_window_id(window_id); + ds->get_key_modifier_state([event modifierFlags], pg); + pg->set_position(wd.mouse_pos); + pg->set_delta(Vector2(-dx, -dy)); + + Input::get_singleton()->parse_input_event(pg); +} + +- (void)scrollWheel:(NSEvent *)event { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + ds->update_mouse_pos(wd, [event locationInWindow]); + + double delta_x = [event scrollingDeltaX]; + double delta_y = [event scrollingDeltaY]; + + if ([event hasPreciseScrollingDeltas]) { + delta_x *= 0.03; + delta_y *= 0.03; + } + + if ([event momentumPhase] != NSEventPhaseNone) { + if (ignore_momentum_scroll) { + return; + } + } else { + ignore_momentum_scroll = false; + } + + if ([event phase] != NSEventPhaseNone || [event momentumPhase] != NSEventPhaseNone) { + [self processPanEvent:event dx:delta_x dy:delta_y]; + } else { + if (fabs(delta_x)) { + [self processScrollEvent:event button:(0 > delta_x ? MouseButton::WHEEL_RIGHT : MouseButton::WHEEL_LEFT) factor:fabs(delta_x * 0.3)]; + } + if (fabs(delta_y)) { + [self processScrollEvent:event button:(0 < delta_y ? MouseButton::WHEEL_UP : MouseButton::WHEEL_DOWN) factor:fabs(delta_y * 0.3)]; + } + } +} + +@end diff --git a/platform/osx/godot_main_osx.mm b/platform/osx/godot_main_osx.mm index 7e7dbf6afb..7fabfaa1b7 100644 --- a/platform/osx/godot_main_osx.mm +++ b/platform/osx/godot_main_osx.mm @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -37,7 +37,7 @@ int main(int argc, char **argv) { #if defined(VULKAN_ENABLED) - // MoltenVK - enable full component swizzling support + // MoltenVK - enable full component swizzling support. setenv("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1", 1); #endif @@ -45,13 +45,14 @@ int main(int argc, char **argv) { const char *dbg_arg = "-NSDocumentRevisionsDebugMode"; printf("arguments\n"); for (int i = 0; i < argc; i++) { - if (strcmp(dbg_arg, argv[i]) == 0) + if (strcmp(dbg_arg, argv[i]) == 0) { first_arg = i + 2; + } printf("%i: %s\n", i, argv[i]); }; #ifdef DEBUG_ENABLED - // lets report the path we made current after all that + // Lets report the path we made current after all that. char cwd[4096]; getcwd(cwd, 4096); printf("Current path: %s\n", cwd); @@ -60,23 +61,25 @@ int main(int argc, char **argv) { OS_OSX os; Error err; - // We must override main when testing is enabled + // We must override main when testing is enabled. TEST_MAIN_OVERRIDE - if (os.open_with_filename != "") { - char *argv_c = (char *)malloc(os.open_with_filename.utf8().size()); - memcpy(argv_c, os.open_with_filename.utf8().get_data(), os.open_with_filename.utf8().size()); + if (os.get_open_with_filename() != "") { + char *argv_c = (char *)malloc(os.get_open_with_filename().utf8().size()); + memcpy(argv_c, os.get_open_with_filename().utf8().get_data(), os.get_open_with_filename().utf8().size()); err = Main::setup(argv[0], 1, &argv_c); free(argv_c); } else { err = Main::setup(argv[0], argc - first_arg, &argv[first_arg]); } - if (err != OK) + if (err != OK) { return 255; + } - if (Main::start()) - os.run(); // it is actually the OS that decides how to run + if (Main::start()) { + os.run(); // It is actually the OS that decides how to run. + } Main::cleanup(); diff --git a/platform/osx/godot_menu_item.h b/platform/osx/godot_menu_item.h new file mode 100644 index 0000000000..50c4709c18 --- /dev/null +++ b/platform/osx/godot_menu_item.h @@ -0,0 +1,52 @@ +/*************************************************************************/ +/* godot_menu_item.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef GODOT_MENU_ITEM_H +#define GODOT_MENU_ITEM_H + +#include "servers/display_server.h" + +#import <AppKit/AppKit.h> +#import <Foundation/Foundation.h> + +@interface GodotMenuItem : NSObject { +@public + Callable callback; + Variant meta; + int id; + bool checkable; +} + +@end + +@implementation GodotMenuItem +@end + +#endif // GODOT_MENU_ITEM_H diff --git a/platform/osx/godot_window.h b/platform/osx/godot_window.h new file mode 100644 index 0000000000..16ff101142 --- /dev/null +++ b/platform/osx/godot_window.h @@ -0,0 +1,47 @@ +/*************************************************************************/ +/* godot_window.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef GODOT_WINDOW_H +#define GODOT_WINDOW_H + +#include "servers/display_server.h" + +#import <AppKit/AppKit.h> +#import <Foundation/Foundation.h> + +@interface GodotWindow : NSWindow { + DisplayServer::WindowID window_id; +} + +- (void)setWindowID:(DisplayServer::WindowID)wid; + +@end + +#endif //GODOT_WINDOW_H diff --git a/platform/osx/godot_window.mm b/platform/osx/godot_window.mm new file mode 100644 index 0000000000..772a2ddb9f --- /dev/null +++ b/platform/osx/godot_window.mm @@ -0,0 +1,69 @@ +/*************************************************************************/ +/* godot_window.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "godot_window.h" + +#include "display_server_osx.h" + +@implementation GodotWindow + +- (id)init { + self = [super init]; + window_id = DisplayServer::INVALID_WINDOW_ID; + return self; +} + +- (void)setWindowID:(DisplayServerOSX::WindowID)wid { + window_id = wid; +} + +- (BOOL)canBecomeKeyWindow { + // Required for NSWindowStyleMaskBorderless windows. + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return YES; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + return !wd.no_focus; +} + +- (BOOL)canBecomeMainWindow { + // Required for NSWindowStyleMaskBorderless windows. + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return YES; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + return !wd.no_focus; +} + +@end diff --git a/platform/osx/godot_window_delegate.h b/platform/osx/godot_window_delegate.h new file mode 100644 index 0000000000..8a1f681fcd --- /dev/null +++ b/platform/osx/godot_window_delegate.h @@ -0,0 +1,47 @@ +/*************************************************************************/ +/* godot_window_delegate.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef GODOT_WINDOW_DELEGATE_H +#define GODOT_WINDOW_DELEGATE_H + +#include "servers/display_server.h" + +#import <AppKit/AppKit.h> +#import <Foundation/Foundation.h> + +@interface GodotWindowDelegate : NSObject <NSWindowDelegate> { + DisplayServer::WindowID window_id; +} + +- (void)setWindowID:(DisplayServer::WindowID)wid; + +@end + +#endif //GODOT_WINDOW_DELEGATE_H diff --git a/platform/osx/godot_window_delegate.mm b/platform/osx/godot_window_delegate.mm new file mode 100644 index 0000000000..1742be987d --- /dev/null +++ b/platform/osx/godot_window_delegate.mm @@ -0,0 +1,254 @@ +/*************************************************************************/ +/* godot_window_delegate.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "godot_window_delegate.h" + +#include "display_server_osx.h" + +@implementation GodotWindowDelegate + +- (void)setWindowID:(DisplayServer::WindowID)wid { + window_id = wid; +} + +- (BOOL)windowShouldClose:(id)sender { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return YES; + } + + ds->send_window_event(ds->get_window(window_id), DisplayServerOSX::WINDOW_EVENT_CLOSE_REQUEST); + return NO; +} + +- (void)windowWillClose:(NSNotification *)notification { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + while (wd.transient_children.size()) { + ds->window_set_transient(wd.transient_children.front()->get(), DisplayServerOSX::INVALID_WINDOW_ID); + } + + if (wd.transient_parent != DisplayServerOSX::INVALID_WINDOW_ID) { + ds->window_set_transient(window_id, DisplayServerOSX::INVALID_WINDOW_ID); + } + + ds->window_destroy(window_id); +} + +- (void)windowDidEnterFullScreen:(NSNotification *)notification { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + wd.fullscreen = true; + // Reset window size limits. + [wd.window_object setContentMinSize:NSMakeSize(0, 0)]; + [wd.window_object setContentMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)]; + + // Force window resize event. + [self windowDidResize:notification]; +} + +- (void)windowDidExitFullScreen:(NSNotification *)notification { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + wd.fullscreen = false; + + // Set window size limits. + const float scale = ds->screen_get_max_scale(); + if (wd.min_size != Size2i()) { + Size2i size = wd.min_size / scale; + [wd.window_object setContentMinSize:NSMakeSize(size.x, size.y)]; + } + if (wd.max_size != Size2i()) { + Size2i size = wd.max_size / scale; + [wd.window_object setContentMaxSize:NSMakeSize(size.x, size.y)]; + } + + // Restore resizability state. + if (wd.resize_disabled) { + [wd.window_object setStyleMask:[wd.window_object styleMask] & ~NSWindowStyleMaskResizable]; + } + + // Restore on-top state. + if (wd.on_top) { + [wd.window_object setLevel:NSFloatingWindowLevel]; + } + + // Force window resize event. + [self windowDidResize:notification]; +} + +- (void)windowDidChangeBackingProperties:(NSNotification *)notification { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + + CGFloat new_scale_factor = [wd.window_object backingScaleFactor]; + CGFloat old_scale_factor = [[[notification userInfo] objectForKey:@"NSBackingPropertyOldScaleFactorKey"] doubleValue]; + + if (new_scale_factor != old_scale_factor) { + // Set new display scale and window size. + const float scale = ds->screen_get_max_scale(); + const NSRect content_rect = [wd.window_view frame]; + + wd.size.width = content_rect.size.width * scale; + wd.size.height = content_rect.size.height * scale; + + ds->send_window_event(wd, DisplayServerOSX::WINDOW_EVENT_DPI_CHANGE); + + CALayer *layer = [wd.window_view layer]; + if (layer) { + layer.contentsScale = scale; + } + + //Force window resize event + [self windowDidResize:notification]; + } +} + +- (void)windowDidResize:(NSNotification *)notification { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + const NSRect content_rect = [wd.window_view frame]; + const float scale = ds->screen_get_max_scale(); + wd.size.width = content_rect.size.width * scale; + wd.size.height = content_rect.size.height * scale; + + CALayer *layer = [wd.window_view layer]; + if (layer) { + layer.contentsScale = scale; + } + + ds->window_resize(window_id, wd.size.width, wd.size.height); + + if (!wd.rect_changed_callback.is_null()) { + Variant size = Rect2i(ds->window_get_position(window_id), ds->window_get_size(window_id)); + Variant *sizep = &size; + Variant ret; + Callable::CallError ce; + wd.rect_changed_callback.call((const Variant **)&sizep, 1, ret, ce); + } +} + +- (void)windowDidMove:(NSNotification *)notification { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + ds->release_pressed_events(); + + if (!wd.rect_changed_callback.is_null()) { + Variant size = Rect2i(ds->window_get_position(window_id), ds->window_get_size(window_id)); + Variant *sizep = &size; + Variant ret; + Callable::CallError ce; + wd.rect_changed_callback.call((const Variant **)&sizep, 1, ret, ce); + } +} + +- (void)windowDidBecomeKey:(NSNotification *)notification { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + + if (ds->mouse_get_mode() == DisplayServer::MOUSE_MODE_CAPTURED) { + const NSRect content_rect = [wd.window_view frame]; + NSRect point_in_window_rect = NSMakeRect(content_rect.size.width / 2, content_rect.size.height / 2, 0, 0); + NSPoint point_on_screen = [[wd.window_view window] convertRectToScreen:point_in_window_rect].origin; + CGPoint mouse_warp_pos = { point_on_screen.x, CGDisplayBounds(CGMainDisplayID()).size.height - point_on_screen.y }; + CGWarpMouseCursorPosition(mouse_warp_pos); + } else { + ds->update_mouse_pos(wd, [wd.window_object mouseLocationOutsideOfEventStream]); + } + + ds->set_last_focused_window(window_id); + ds->send_window_event(wd, DisplayServerOSX::WINDOW_EVENT_FOCUS_IN); +} + +- (void)windowDidResignKey:(NSNotification *)notification { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + + ds->release_pressed_events(); + ds->send_window_event(wd, DisplayServerOSX::WINDOW_EVENT_FOCUS_OUT); +} + +- (void)windowDidMiniaturize:(NSNotification *)notification { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + + ds->release_pressed_events(); + ds->send_window_event(wd, DisplayServerOSX::WINDOW_EVENT_FOCUS_OUT); +} + +- (void)windowDidDeminiaturize:(NSNotification *)notification { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (!ds || !ds->has_window(window_id)) { + return; + } + + DisplayServerOSX::WindowData &wd = ds->get_window(window_id); + + ds->set_last_focused_window(window_id); + ds->send_window_event(wd, DisplayServerOSX::WINDOW_EVENT_FOCUS_IN); +} + +@end diff --git a/platform/osx/joypad_osx.cpp b/platform/osx/joypad_osx.cpp index 48d165d30b..d518206f04 100644 --- a/platform/osx/joypad_osx.cpp +++ b/platform/osx/joypad_osx.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -80,7 +80,7 @@ int joypad::get_hid_element_state(rec_element *p_element) const { if (IOHIDDeviceGetValue(device_ref, p_element->ref, &valueRef) == kIOReturnSuccess) { value = (SInt32)IOHIDValueGetIntegerValue(valueRef); - /* record min and max for auto calibration */ + // Record min and max for auto calibration. if (value < p_element->min) { p_element->min = value; } @@ -179,7 +179,7 @@ void joypad::add_hid_element(IOHIDElementRef p_element) { break; } - if (list) { /* add to list */ + if (list) { // Add to list. rec_element element; element.ref = p_element; @@ -280,7 +280,7 @@ static String _hex_str(uint8_t p_byte) { bool JoypadOSX::configure_joypad(IOHIDDeviceRef p_device_ref, joypad *p_joy) { p_joy->device_ref = p_device_ref; - /* get device name */ + // Get device name. String name; char c_name[256]; CFTypeRef refCF = IOHIDDeviceGetProperty(p_device_ref, CFSTR(kIOHIDProductKey)); @@ -319,7 +319,7 @@ bool JoypadOSX::configure_joypad(IOHIDDeviceRef p_device_ref, joypad *p_joy) { sprintf(uid, "%08x%08x%08x%08x", OSSwapHostToBigInt32(3), OSSwapHostToBigInt32(vendor), OSSwapHostToBigInt32(product_id), OSSwapHostToBigInt32(version)); input->joy_connection_changed(id, true, name, uid); } else { - //bluetooth device + // Bluetooth device. String guid = "05000000"; for (int i = 0; i < 12; i++) { if (i < name.size()) @@ -445,24 +445,13 @@ static HatMask process_hat_value(int p_min, int p_max, int p_value, bool p_offse void JoypadOSX::poll_joypads() const { while (CFRunLoopRunInMode(GODOT_JOY_LOOP_RUN_MODE, 0, TRUE) == kCFRunLoopRunHandledSource) { - /* no-op. Pending callbacks will fire. */ + // No-op. Pending callbacks will fire. } } -static const Input::JoyAxisValue axis_correct(int p_value, int p_min, int p_max) { - Input::JoyAxisValue jx; - if (p_min < 0) { - jx.min = -1; - if (p_value < 0) { - jx.value = (float)-p_value / p_min; - } else - jx.value = (float)p_value / p_max; - } - if (p_min == 0) { - jx.min = 0; - jx.value = 0.0f + (float)p_value / p_max; - } - return jx; +static float axis_correct(int p_value, int p_min, int p_max) { + // Convert to a value between -1.0f and 1.0f. + return 2.0f * (p_value - p_min) / (p_max - p_min) - 1.0f; } void JoypadOSX::process_joypads() { @@ -579,7 +568,7 @@ void JoypadOSX::config_hid_manager(CFArrayRef p_matching_array) const { IOHIDManagerScheduleWithRunLoop(hid_manager, runloop, GODOT_JOY_LOOP_RUN_MODE); while (CFRunLoopRunInMode(GODOT_JOY_LOOP_RUN_MODE, 0, TRUE) == kCFRunLoopRunHandledSource) { - /* no-op. Callback fires once per existing device. */ + // No-op. Callback fires once per existing device. } } diff --git a/platform/osx/joypad_osx.h b/platform/osx/joypad_osx.h index 2ba7f0d950..4ca7fb1698 100644 --- a/platform/osx/joypad_osx.h +++ b/platform/osx/joypad_osx.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -66,7 +66,7 @@ struct joypad { int id = 0; bool offset_hat = false; - io_service_t ffservice = 0; /* Interface for force feedback, 0 = no ff */ + io_service_t ffservice = 0; // Interface for force feedback, 0 = no ff. FFCONSTANTFORCE ff_constant_force; FFDeviceObjectReference ff_device = nullptr; FFEffectObjectReference ff_object = nullptr; diff --git a/platform/osx/key_mapping_osx.h b/platform/osx/key_mapping_osx.h new file mode 100644 index 0000000000..252cc907bb --- /dev/null +++ b/platform/osx/key_mapping_osx.h @@ -0,0 +1,52 @@ +/*************************************************************************/ +/* key_mapping_osx.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef KEY_MAPPING_OSX_H +#define KEY_MAPPING_OSX_H + +#include "core/os/keyboard.h" + +class KeyMappingOSX { + KeyMappingOSX() {} + + static bool is_numpad_key(unsigned int key); + +public: + // Mappings input. + static Key translate_key(unsigned int key); + static unsigned int unmap_key(Key key); + static Key remap_key(unsigned int key, unsigned int state); + + // Mapping for menu shortcuts. + static String keycode_get_native_string(Key p_keycode); + static unsigned int keycode_get_native_mask(Key p_keycode); +}; + +#endif // KEY_MAPPING_OSX_H diff --git a/platform/osx/key_mapping_osx.mm b/platform/osx/key_mapping_osx.mm new file mode 100644 index 0000000000..fde9206824 --- /dev/null +++ b/platform/osx/key_mapping_osx.mm @@ -0,0 +1,477 @@ +/*************************************************************************/ +/* key_mapping_osx.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "key_mapping_osx.h" + +#include <Carbon/Carbon.h> +#include <Cocoa/Cocoa.h> + +bool KeyMappingOSX::is_numpad_key(unsigned int key) { + static const unsigned int table[] = { + 0x41, /* kVK_ANSI_KeypadDecimal */ + 0x43, /* kVK_ANSI_KeypadMultiply */ + 0x45, /* kVK_ANSI_KeypadPlus */ + 0x47, /* kVK_ANSI_KeypadClear */ + 0x4b, /* kVK_ANSI_KeypadDivide */ + 0x4c, /* kVK_ANSI_KeypadEnter */ + 0x4e, /* kVK_ANSI_KeypadMinus */ + 0x51, /* kVK_ANSI_KeypadEquals */ + 0x52, /* kVK_ANSI_Keypad0 */ + 0x53, /* kVK_ANSI_Keypad1 */ + 0x54, /* kVK_ANSI_Keypad2 */ + 0x55, /* kVK_ANSI_Keypad3 */ + 0x56, /* kVK_ANSI_Keypad4 */ + 0x57, /* kVK_ANSI_Keypad5 */ + 0x58, /* kVK_ANSI_Keypad6 */ + 0x59, /* kVK_ANSI_Keypad7 */ + 0x5b, /* kVK_ANSI_Keypad8 */ + 0x5c, /* kVK_ANSI_Keypad9 */ + 0x5f, /* kVK_JIS_KeypadComma */ + 0x00 + }; + for (int i = 0; table[i] != 0; i++) { + if (key == table[i]) { + return true; + } + } + return false; +} + +// Keyboard symbol translation table. +static const Key _osx_to_godot_table[128] = { + /* 00 */ Key::A, + /* 01 */ Key::S, + /* 02 */ Key::D, + /* 03 */ Key::F, + /* 04 */ Key::H, + /* 05 */ Key::G, + /* 06 */ Key::Z, + /* 07 */ Key::X, + /* 08 */ Key::C, + /* 09 */ Key::V, + /* 0a */ Key::SECTION, /* ISO Section */ + /* 0b */ Key::B, + /* 0c */ Key::Q, + /* 0d */ Key::W, + /* 0e */ Key::E, + /* 0f */ Key::R, + /* 10 */ Key::Y, + /* 11 */ Key::T, + /* 12 */ Key::KEY_1, + /* 13 */ Key::KEY_2, + /* 14 */ Key::KEY_3, + /* 15 */ Key::KEY_4, + /* 16 */ Key::KEY_6, + /* 17 */ Key::KEY_5, + /* 18 */ Key::EQUAL, + /* 19 */ Key::KEY_9, + /* 1a */ Key::KEY_7, + /* 1b */ Key::MINUS, + /* 1c */ Key::KEY_8, + /* 1d */ Key::KEY_0, + /* 1e */ Key::BRACERIGHT, + /* 1f */ Key::O, + /* 20 */ Key::U, + /* 21 */ Key::BRACELEFT, + /* 22 */ Key::I, + /* 23 */ Key::P, + /* 24 */ Key::ENTER, + /* 25 */ Key::L, + /* 26 */ Key::J, + /* 27 */ Key::APOSTROPHE, + /* 28 */ Key::K, + /* 29 */ Key::SEMICOLON, + /* 2a */ Key::BACKSLASH, + /* 2b */ Key::COMMA, + /* 2c */ Key::SLASH, + /* 2d */ Key::N, + /* 2e */ Key::M, + /* 2f */ Key::PERIOD, + /* 30 */ Key::TAB, + /* 31 */ Key::SPACE, + /* 32 */ Key::QUOTELEFT, + /* 33 */ Key::BACKSPACE, + /* 34 */ Key::UNKNOWN, + /* 35 */ Key::ESCAPE, + /* 36 */ Key::META, + /* 37 */ Key::META, + /* 38 */ Key::SHIFT, + /* 39 */ Key::CAPSLOCK, + /* 3a */ Key::ALT, + /* 3b */ Key::CTRL, + /* 3c */ Key::SHIFT, + /* 3d */ Key::ALT, + /* 3e */ Key::CTRL, + /* 3f */ Key::UNKNOWN, /* Function */ + /* 40 */ Key::UNKNOWN, /* F17 */ + /* 41 */ Key::KP_PERIOD, + /* 42 */ Key::UNKNOWN, + /* 43 */ Key::KP_MULTIPLY, + /* 44 */ Key::UNKNOWN, + /* 45 */ Key::KP_ADD, + /* 46 */ Key::UNKNOWN, + /* 47 */ Key::NUMLOCK, /* Really KeypadClear... */ + /* 48 */ Key::VOLUMEUP, /* VolumeUp */ + /* 49 */ Key::VOLUMEDOWN, /* VolumeDown */ + /* 4a */ Key::VOLUMEMUTE, /* Mute */ + /* 4b */ Key::KP_DIVIDE, + /* 4c */ Key::KP_ENTER, + /* 4d */ Key::UNKNOWN, + /* 4e */ Key::KP_SUBTRACT, + /* 4f */ Key::UNKNOWN, /* F18 */ + /* 50 */ Key::UNKNOWN, /* F19 */ + /* 51 */ Key::EQUAL, /* KeypadEqual */ + /* 52 */ Key::KP_0, + /* 53 */ Key::KP_1, + /* 54 */ Key::KP_2, + /* 55 */ Key::KP_3, + /* 56 */ Key::KP_4, + /* 57 */ Key::KP_5, + /* 58 */ Key::KP_6, + /* 59 */ Key::KP_7, + /* 5a */ Key::UNKNOWN, /* F20 */ + /* 5b */ Key::KP_8, + /* 5c */ Key::KP_9, + /* 5d */ Key::YEN, /* JIS Yen */ + /* 5e */ Key::UNDERSCORE, /* JIS Underscore */ + /* 5f */ Key::COMMA, /* JIS KeypadComma */ + /* 60 */ Key::F5, + /* 61 */ Key::F6, + /* 62 */ Key::F7, + /* 63 */ Key::F3, + /* 64 */ Key::F8, + /* 65 */ Key::F9, + /* 66 */ Key::UNKNOWN, /* JIS Eisu */ + /* 67 */ Key::F11, + /* 68 */ Key::UNKNOWN, /* JIS Kana */ + /* 69 */ Key::F13, + /* 6a */ Key::F16, + /* 6b */ Key::F14, + /* 6c */ Key::UNKNOWN, + /* 6d */ Key::F10, + /* 6e */ Key::MENU, + /* 6f */ Key::F12, + /* 70 */ Key::UNKNOWN, + /* 71 */ Key::F15, + /* 72 */ Key::INSERT, /* Really Help... */ + /* 73 */ Key::HOME, + /* 74 */ Key::PAGEUP, + /* 75 */ Key::KEY_DELETE, + /* 76 */ Key::F4, + /* 77 */ Key::END, + /* 78 */ Key::F2, + /* 79 */ Key::PAGEDOWN, + /* 7a */ Key::F1, + /* 7b */ Key::LEFT, + /* 7c */ Key::RIGHT, + /* 7d */ Key::DOWN, + /* 7e */ Key::UP, + /* 7f */ Key::UNKNOWN, +}; + +// Translates a OS X keycode to a Godot keycode. +Key KeyMappingOSX::translate_key(unsigned int key) { + if (key >= 128) { + return Key::UNKNOWN; + } + + return _osx_to_godot_table[key]; +} + +// Translates a Godot keycode back to a OSX keycode. +unsigned int KeyMappingOSX::unmap_key(Key key) { + for (int i = 0; i <= 126; i++) { + if (_osx_to_godot_table[i] == key) { + return i; + } + } + return 127; +} + +struct _KeyCodeMap { + UniChar kchar; + Key kcode; +}; + +static const _KeyCodeMap _keycodes[55] = { + { '`', Key::QUOTELEFT }, + { '~', Key::ASCIITILDE }, + { '0', Key::KEY_0 }, + { '1', Key::KEY_1 }, + { '2', Key::KEY_2 }, + { '3', Key::KEY_3 }, + { '4', Key::KEY_4 }, + { '5', Key::KEY_5 }, + { '6', Key::KEY_6 }, + { '7', Key::KEY_7 }, + { '8', Key::KEY_8 }, + { '9', Key::KEY_9 }, + { '-', Key::MINUS }, + { '_', Key::UNDERSCORE }, + { '=', Key::EQUAL }, + { '+', Key::PLUS }, + { 'q', Key::Q }, + { 'w', Key::W }, + { 'e', Key::E }, + { 'r', Key::R }, + { 't', Key::T }, + { 'y', Key::Y }, + { 'u', Key::U }, + { 'i', Key::I }, + { 'o', Key::O }, + { 'p', Key::P }, + { '[', Key::BRACELEFT }, + { ']', Key::BRACERIGHT }, + { '{', Key::BRACELEFT }, + { '}', Key::BRACERIGHT }, + { 'a', Key::A }, + { 's', Key::S }, + { 'd', Key::D }, + { 'f', Key::F }, + { 'g', Key::G }, + { 'h', Key::H }, + { 'j', Key::J }, + { 'k', Key::K }, + { 'l', Key::L }, + { ';', Key::SEMICOLON }, + { ':', Key::COLON }, + { '\'', Key::APOSTROPHE }, + { '\"', Key::QUOTEDBL }, + { '\\', Key::BACKSLASH }, + { '#', Key::NUMBERSIGN }, + { 'z', Key::Z }, + { 'x', Key::X }, + { 'c', Key::C }, + { 'v', Key::V }, + { 'b', Key::B }, + { 'n', Key::N }, + { 'm', Key::M }, + { ',', Key::COMMA }, + { '.', Key::PERIOD }, + { '/', Key::SLASH } +}; + +// Remap key according to current keyboard layout. +Key KeyMappingOSX::remap_key(unsigned int key, unsigned int state) { + if (is_numpad_key(key)) { + return translate_key(key); + } + + TISInputSourceRef current_keyboard = TISCopyCurrentKeyboardInputSource(); + if (!current_keyboard) { + return translate_key(key); + } + + CFDataRef layout_data = (CFDataRef)TISGetInputSourceProperty(current_keyboard, kTISPropertyUnicodeKeyLayoutData); + if (!layout_data) { + return translate_key(key); + } + + const UCKeyboardLayout *keyboard_layout = (const UCKeyboardLayout *)CFDataGetBytePtr(layout_data); + + UInt32 keys_down = 0; + UniChar chars[4]; + UniCharCount real_length; + + OSStatus err = UCKeyTranslate(keyboard_layout, + key, + kUCKeyActionDisplay, + (state >> 8) & 0xFF, + LMGetKbdType(), + kUCKeyTranslateNoDeadKeysBit, + &keys_down, + sizeof(chars) / sizeof(chars[0]), + &real_length, + chars); + + if (err != noErr) { + return translate_key(key); + } + + for (unsigned int i = 0; i < 55; i++) { + if (_keycodes[i].kchar == chars[0]) { + return _keycodes[i].kcode; + } + } + return translate_key(key); +} + +struct _KeyCodeText { + Key code; + char32_t text; +}; + +static const _KeyCodeText _native_keycodes[] = { + /* clang-format off */ + {Key::ESCAPE ,0x001B}, + {Key::TAB ,0x0009}, + {Key::BACKTAB ,0x007F}, + {Key::BACKSPACE ,0x0008}, + {Key::ENTER ,0x000D}, + {Key::INSERT ,NSInsertFunctionKey}, + {Key::KEY_DELETE ,0x007F}, + {Key::PAUSE ,NSPauseFunctionKey}, + {Key::PRINT ,NSPrintScreenFunctionKey}, + {Key::SYSREQ ,NSSysReqFunctionKey}, + {Key::CLEAR ,NSClearLineFunctionKey}, + {Key::HOME ,0x2196}, + {Key::END ,0x2198}, + {Key::LEFT ,0x001C}, + {Key::UP ,0x001E}, + {Key::RIGHT ,0x001D}, + {Key::DOWN ,0x001F}, + {Key::PAGEUP ,0x21DE}, + {Key::PAGEDOWN ,0x21DF}, + {Key::NUMLOCK ,NSClearLineFunctionKey}, + {Key::SCROLLLOCK ,NSScrollLockFunctionKey}, + {Key::F1 ,NSF1FunctionKey}, + {Key::F2 ,NSF2FunctionKey}, + {Key::F3 ,NSF3FunctionKey}, + {Key::F4 ,NSF4FunctionKey}, + {Key::F5 ,NSF5FunctionKey}, + {Key::F6 ,NSF6FunctionKey}, + {Key::F7 ,NSF7FunctionKey}, + {Key::F8 ,NSF8FunctionKey}, + {Key::F9 ,NSF9FunctionKey}, + {Key::F10 ,NSF10FunctionKey}, + {Key::F11 ,NSF11FunctionKey}, + {Key::F12 ,NSF12FunctionKey}, + {Key::F13 ,NSF13FunctionKey}, + {Key::F14 ,NSF14FunctionKey}, + {Key::F15 ,NSF15FunctionKey}, + {Key::F16 ,NSF16FunctionKey}, //* ... NSF35FunctionKey */ + {Key::MENU ,NSMenuFunctionKey}, + {Key::HELP ,NSHelpFunctionKey}, + {Key::STOP ,NSStopFunctionKey}, + {Key::LAUNCH0 ,NSUserFunctionKey}, + {Key::SPACE ,0x0020}, + {Key::EXCLAM ,'!'}, + {Key::QUOTEDBL ,'\"'}, + {Key::NUMBERSIGN ,'#'}, + {Key::DOLLAR ,'$'}, + {Key::PERCENT ,'\%'}, + {Key::AMPERSAND ,'&'}, + {Key::APOSTROPHE ,'\''}, + {Key::PARENLEFT ,'('}, + {Key::PARENRIGHT ,')'}, + {Key::ASTERISK ,'*'}, + {Key::PLUS ,'+'}, + {Key::COMMA ,','}, + {Key::MINUS ,'-'}, + {Key::PERIOD ,'.'}, + {Key::SLASH ,'/'}, + {Key::KEY_0 ,'0'}, + {Key::KEY_1 ,'1'}, + {Key::KEY_2 ,'2'}, + {Key::KEY_3 ,'3'}, + {Key::KEY_4 ,'4'}, + {Key::KEY_5 ,'5'}, + {Key::KEY_6 ,'6'}, + {Key::KEY_7 ,'7'}, + {Key::KEY_8 ,'8'}, + {Key::KEY_9 ,'9'}, + {Key::COLON ,':'}, + {Key::SEMICOLON ,';'}, + {Key::LESS ,'<'}, + {Key::EQUAL ,'='}, + {Key::GREATER ,'>'}, + {Key::QUESTION ,'?'}, + {Key::AT ,'@'}, + {Key::A ,'a'}, + {Key::B ,'b'}, + {Key::C ,'c'}, + {Key::D ,'d'}, + {Key::E ,'e'}, + {Key::F ,'f'}, + {Key::G ,'g'}, + {Key::H ,'h'}, + {Key::I ,'i'}, + {Key::J ,'j'}, + {Key::K ,'k'}, + {Key::L ,'l'}, + {Key::M ,'m'}, + {Key::N ,'n'}, + {Key::O ,'o'}, + {Key::P ,'p'}, + {Key::Q ,'q'}, + {Key::R ,'r'}, + {Key::S ,'s'}, + {Key::T ,'t'}, + {Key::U ,'u'}, + {Key::V ,'v'}, + {Key::W ,'w'}, + {Key::X ,'x'}, + {Key::Y ,'y'}, + {Key::Z ,'z'}, + {Key::BRACKETLEFT ,'['}, + {Key::BACKSLASH ,'\\'}, + {Key::BRACKETRIGHT ,']'}, + {Key::ASCIICIRCUM ,'^'}, + {Key::UNDERSCORE ,'_'}, + {Key::QUOTELEFT ,'`'}, + {Key::BRACELEFT ,'{'}, + {Key::BAR ,'|'}, + {Key::BRACERIGHT ,'}'}, + {Key::ASCIITILDE ,'~'}, + {Key::NONE ,0x0000} + /* clang-format on */ +}; + +String KeyMappingOSX::keycode_get_native_string(Key p_keycode) { + const _KeyCodeText *kct = &_native_keycodes[0]; + + while (kct->text) { + if (kct->code == p_keycode) { + return String::chr(kct->text); + } + kct++; + } + return String(); +} + +unsigned int KeyMappingOSX::keycode_get_native_mask(Key p_keycode) { + unsigned int mask = 0; + if ((p_keycode & KeyModifierMask::CTRL) != Key::NONE) { + mask |= NSEventModifierFlagControl; + } + if ((p_keycode & KeyModifierMask::ALT) != Key::NONE) { + mask |= NSEventModifierFlagOption; + } + if ((p_keycode & KeyModifierMask::SHIFT) != Key::NONE) { + mask |= NSEventModifierFlagShift; + } + if ((p_keycode & KeyModifierMask::META) != Key::NONE) { + mask |= NSEventModifierFlagCommand; + } + if ((p_keycode & KeyModifierMask::KPAD) != Key::NONE) { + mask |= NSEventModifierFlagNumericPad; + } + return mask; +} diff --git a/platform/osx/os_osx.h b/platform/osx/os_osx.h index 7e02f4e154..5bb5b3320e 100644 --- a/platform/osx/os_osx.h +++ b/platform/osx/os_osx.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -40,9 +40,7 @@ #include "servers/audio_server.h" class OS_OSX : public OS_Unix { - virtual void delete_main_loop() override; - - bool force_quit; + bool force_quit = false; JoypadOSX *joypad_osx = nullptr; @@ -55,13 +53,15 @@ class OS_OSX : public OS_Unix { CrashHandler crash_handler; - MainLoop *main_loop; + CFRunLoopObserverRef pre_wait_observer; - static void pre_wait_observer_cb(CFRunLoopObserverRef p_observer, CFRunLoopActivity p_activiy, void *p_context); + MainLoop *main_loop = nullptr; -public: String open_with_filename; + static _FORCE_INLINE_ String get_framework_executable(const String &p_path); + static void pre_wait_observer_cb(CFRunLoopObserverRef p_observer, CFRunLoopActivity p_activiy, void *p_context); + protected: virtual void initialize_core() override; virtual void initialize() override; @@ -70,8 +70,12 @@ protected: virtual void initialize_joypads() override; virtual void set_main_loop(MainLoop *p_main_loop) override; + virtual void delete_main_loop() override; public: + String get_open_with_filename() const; + void set_open_with_filename(const String &p_path); + virtual String get_name() const override; virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override; @@ -89,26 +93,27 @@ public: virtual String get_system_dir(SystemDir p_dir, bool p_shared_storage = true) const override; - Error shell_open(String p_uri) override; + virtual Error shell_open(String p_uri) override; - String get_locale() const override; + virtual String get_locale() const override; virtual String get_executable_path() const override; - virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr) override; + virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override; virtual Error create_instance(const List<String> &p_arguments, ProcessID *r_child_id = nullptr) override; - virtual String get_unique_id() const override; //++ + virtual String get_unique_id() const override; virtual bool _check_internal_feature_support(const String &p_feature) override; - void run(); - virtual void disable_crash_handler() override; virtual bool is_disable_crash_handler() const override; virtual Error move_to_trash(const String &p_path) override; + void run(); + OS_OSX(); + ~OS_OSX(); }; #endif diff --git a/platform/osx/os_osx.mm b/platform/osx/os_osx.mm index 39608bdea8..9288e658cf 100644 --- a/platform/osx/os_osx.mm +++ b/platform/osx/os_osx.mm @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -31,241 +31,45 @@ #include "os_osx.h" #include "core/version_generated.gen.h" +#include "main/main.h" #include "dir_access_osx.h" #include "display_server_osx.h" -#include "main/main.h" +#include "godot_application.h" +#include "godot_application_delegate.h" +#include "osx_terminal_logger.h" #include <dlfcn.h> #include <libproc.h> #include <mach-o/dyld.h> -#include <os/log.h> - -#define DS_OSX ((DisplayServerOSX *)(DisplayServerOSX::get_singleton())) - -/*************************************************************************/ -/* GodotApplication */ -/*************************************************************************/ - -@interface GodotApplication : NSApplication -@end - -@implementation GodotApplication - -- (void)sendEvent:(NSEvent *)event { - if (DS_OSX) { - DS_OSX->_send_event(event); - } - - // From http://cocoadev.com/index.pl?GameKeyboardHandlingAlmost - // This works around an AppKit bug, where key up events while holding - // down the command key don't get sent to the key window. - if ([event type] == NSEventTypeKeyUp && ([event modifierFlags] & NSEventModifierFlagCommand)) { - [[self keyWindow] sendEvent:event]; - } else { - [super sendEvent:event]; - } -} - -@end - -/*************************************************************************/ -/* GodotApplicationDelegate */ -/*************************************************************************/ - -@interface GodotApplicationDelegate : NSObject -- (void)forceUnbundledWindowActivationHackStep1; -- (void)forceUnbundledWindowActivationHackStep2; -- (void)forceUnbundledWindowActivationHackStep3; -@end - -@implementation GodotApplicationDelegate - -- (void)forceUnbundledWindowActivationHackStep1 { - // Step1: Switch focus to macOS Dock. - // Required to perform step 2, TransformProcessType will fail if app is already the in focus. - for (NSRunningApplication *app in [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.dock"]) { - [app activateWithOptions:NSApplicationActivateIgnoringOtherApps]; - break; - } - [self performSelector:@selector(forceUnbundledWindowActivationHackStep2) - withObject:nil - afterDelay:0.02]; -} - -- (void)forceUnbundledWindowActivationHackStep2 { - // Step 2: Register app as foreground process. - ProcessSerialNumber psn = { 0, kCurrentProcess }; - (void)TransformProcessType(&psn, kProcessTransformToForegroundApplication); - [self performSelector:@selector(forceUnbundledWindowActivationHackStep3) withObject:nil afterDelay:0.02]; -} - -- (void)forceUnbundledWindowActivationHackStep3 { - // Step 3: Switch focus back to app window. - [[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps]; -} - -- (void)applicationDidFinishLaunching:(NSNotification *)notice { - NSString *nsappname = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"]; - if (nsappname == nil) { - // If executable is not a bundled, macOS WindowServer won't register and activate app window correctly (menu and title bar are grayed out and input ignored). - [self performSelector:@selector(forceUnbundledWindowActivationHackStep1) withObject:nil afterDelay:0.02]; - } -} - -- (void)applicationDidResignActive:(NSNotification *)notification { - if (OS::get_singleton()->get_main_loop()) { - OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_OUT); - } -} - -- (void)applicationDidBecomeActive:(NSNotification *)notification { - if (OS::get_singleton()->get_main_loop()) { - OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_IN); - } -} -- (void)globalMenuCallback:(id)sender { - if (DS_OSX) { - return DS_OSX->_menu_callback(sender); - } -} - -- (NSMenu *)applicationDockMenu:(NSApplication *)sender { - if (DS_OSX) { - return DS_OSX->_get_dock_menu(); +_FORCE_INLINE_ String OS_OSX::get_framework_executable(const String &p_path) { + // Append framework executable name, or return as is if p_path is not a framework. + DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (da->dir_exists(p_path) && da->file_exists(p_path.plus_file(p_path.get_file().get_basename()))) { + return p_path.plus_file(p_path.get_file().get_basename()); } else { - return nullptr; - } -} - -- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename { - // Note: may be called called before main loop init! - char *utfs = strdup([filename UTF8String]); - ((OS_OSX *)OS_OSX::get_singleton())->open_with_filename.parse_utf8(utfs); - free(utfs); - -#ifdef TOOLS_ENABLED - // Open new instance - if (OS_OSX::get_singleton()->get_main_loop()) { - List<String> args; - args.push_back(((OS_OSX *)OS_OSX::get_singleton())->open_with_filename); - String exec = OS_OSX::get_singleton()->get_executable_path(); - OS_OSX::get_singleton()->create_process(exec, args); - } -#endif - return YES; -} - -- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { - if (DS_OSX) { - DS_OSX->_send_window_event(DS_OSX->windows[DisplayServerOSX::MAIN_WINDOW_ID], DisplayServerOSX::WINDOW_EVENT_CLOSE_REQUEST); - } - return NSTerminateCancel; -} - -- (void)showAbout:(id)sender { - if (OS_OSX::get_singleton()->get_main_loop()) { - OS_OSX::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_WM_ABOUT); + return p_path; } } -@end - -/*************************************************************************/ -/* OSXTerminalLogger */ -/*************************************************************************/ - -class OSXTerminalLogger : public StdLogger { -public: - virtual void log_error(const char *p_function, const char *p_file, int p_line, const char *p_code, const char *p_rationale, bool p_editor_notify = false, ErrorType p_type = ERR_ERROR) { - if (!should_log(true)) { - return; - } - - const char *err_details; - if (p_rationale && p_rationale[0]) - err_details = p_rationale; - else - err_details = p_code; - - switch (p_type) { - case ERR_WARNING: - os_log_info(OS_LOG_DEFAULT, - "WARNING: %{public}s\nat: %{public}s (%{public}s:%i)", - err_details, p_function, p_file, p_line); - logf_error("\E[1;33mWARNING:\E[0;93m %s\n", err_details); - logf_error("\E[0;90m at: %s (%s:%i)\E[0m\n", p_function, p_file, p_line); - break; - case ERR_SCRIPT: - os_log_error(OS_LOG_DEFAULT, - "SCRIPT ERROR: %{public}s\nat: %{public}s (%{public}s:%i)", - err_details, p_function, p_file, p_line); - logf_error("\E[1;35mSCRIPT ERROR:\E[0;95m %s\n", err_details); - logf_error("\E[0;90m at: %s (%s:%i)\E[0m\n", p_function, p_file, p_line); - break; - case ERR_SHADER: - os_log_error(OS_LOG_DEFAULT, - "SHADER ERROR: %{public}s\nat: %{public}s (%{public}s:%i)", - err_details, p_function, p_file, p_line); - logf_error("\E[1;36mSHADER ERROR:\E[0;96m %s\n", err_details); - logf_error("\E[0;90m at: %s (%s:%i)\E[0m\n", p_function, p_file, p_line); - break; - case ERR_ERROR: - default: - os_log_error(OS_LOG_DEFAULT, - "ERROR: %{public}s\nat: %{public}s (%{public}s:%i)", - err_details, p_function, p_file, p_line); - logf_error("\E[1;31mERROR:\E[0;91m %s\n", err_details); - logf_error("\E[0;90m at: %s (%s:%i)\E[0m\n", p_function, p_file, p_line); - break; - } - } -}; - -/*************************************************************************/ -/* OS_OSX */ -/*************************************************************************/ - -String OS_OSX::get_unique_id() const { - static String serial_number; - - if (serial_number.is_empty()) { - io_service_t platformExpert = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice")); - CFStringRef serialNumberAsCFString = nullptr; - if (platformExpert) { - serialNumberAsCFString = (CFStringRef)IORegistryEntryCreateCFProperty(platformExpert, CFSTR(kIOPlatformSerialNumberKey), kCFAllocatorDefault, 0); - IOObjectRelease(platformExpert); - } +void OS_OSX::pre_wait_observer_cb(CFRunLoopObserverRef p_observer, CFRunLoopActivity p_activiy, void *p_context) { + // Prevent main loop from sleeping and redraw window during resize / modal popups. - NSString *serialNumberAsNSString = nil; - if (serialNumberAsCFString) { - serialNumberAsNSString = [NSString stringWithString:(NSString *)serialNumberAsCFString]; - CFRelease(serialNumberAsCFString); + if (get_singleton()->get_main_loop()) { + Main::force_redraw(); + if (!Main::is_iterating()) { // Avoid cyclic loop. + Main::iteration(); } - - serial_number = [serialNumberAsNSString UTF8String]; } - return serial_number; + CFRunLoopWakeUp(CFRunLoopGetCurrent()); // Prevent main loop from sleeping. } -void OS_OSX::alert(const String &p_alert, const String &p_title) { - NSAlert *window = [[NSAlert alloc] init]; - NSString *ns_title = [NSString stringWithUTF8String:p_title.utf8().get_data()]; - NSString *ns_alert = [NSString stringWithUTF8String:p_alert.utf8().get_data()]; - - [window addButtonWithTitle:@"OK"]; - [window setMessageText:ns_title]; - [window setInformativeText:ns_alert]; - [window setAlertStyle:NSAlertStyleWarning]; +void OS_OSX::initialize() { + crash_handler.initialize(); - id key_window = [[NSApplication sharedApplication] keyWindow]; - [window runModal]; - [window release]; - if (key_window) { - [key_window makeKeyAndOrderFront:nil]; - } + initialize_core(); } void OS_OSX::initialize_core() { @@ -276,17 +80,6 @@ void OS_OSX::initialize_core() { DirAccess::make_default<DirAccessOSX>(DirAccess::ACCESS_FILESYSTEM); } -void OS_OSX::initialize_joypads() { - joypad_osx = memnew(JoypadOSX(Input::get_singleton())); -} - -void OS_OSX::initialize() { - crash_handler.initialize(); - - initialize_core(); - //ensure_user_data_dir(); -} - void OS_OSX::finalize() { #ifdef COREMIDI_ENABLED midi_driver.close(); @@ -299,42 +92,63 @@ void OS_OSX::finalize() { } } +void OS_OSX::initialize_joypads() { + joypad_osx = memnew(JoypadOSX(Input::get_singleton())); +} + void OS_OSX::set_main_loop(MainLoop *p_main_loop) { main_loop = p_main_loop; } void OS_OSX::delete_main_loop() { - if (!main_loop) + if (!main_loop) { return; + } + memdelete(main_loop); main_loop = nullptr; } +String OS_OSX::get_open_with_filename() const { + return open_with_filename; +} + +void OS_OSX::set_open_with_filename(const String &p_path) { + open_with_filename = p_path; +} + String OS_OSX::get_name() const { return "macOS"; } -_FORCE_INLINE_ String _get_framework_executable(const String p_path) { - // Append framework executable name, or return as is if p_path is not a framework. - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - if (da->dir_exists(p_path) && da->file_exists(p_path.plus_file(p_path.get_file().get_basename()))) { - return p_path.plus_file(p_path.get_file().get_basename()); - } else { - return p_path; +void OS_OSX::alert(const String &p_alert, const String &p_title) { + NSAlert *window = [[NSAlert alloc] init]; + NSString *ns_title = [NSString stringWithUTF8String:p_title.utf8().get_data()]; + NSString *ns_alert = [NSString stringWithUTF8String:p_alert.utf8().get_data()]; + + [window addButtonWithTitle:@"OK"]; + [window setMessageText:ns_title]; + [window setInformativeText:ns_alert]; + [window setAlertStyle:NSAlertStyleWarning]; + + id key_window = [[NSApplication sharedApplication] keyWindow]; + [window runModal]; + if (key_window) { + [key_window makeKeyAndOrderFront:nil]; } } Error OS_OSX::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { - String path = _get_framework_executable(p_path); + String path = get_framework_executable(p_path); if (!FileAccess::exists(path)) { - // This code exists so gdnative can load .dylib files from within the executable path. - path = _get_framework_executable(get_executable_path().get_base_dir().plus_file(p_path.get_file())); + // Load .dylib or framework from within the executable path. + path = get_framework_executable(get_executable_path().get_base_dir().plus_file(p_path.get_file())); } if (!FileAccess::exists(path)) { - // This code exists so gdnative can load .dylib files from a standard macOS location. - path = _get_framework_executable(get_executable_path().get_base_dir().plus_file("../Frameworks").plus_file(p_path.get_file())); + // Load .dylib or framework from a standard macOS location. + path = get_framework_executable(get_executable_path().get_base_dir().plus_file("../Frameworks").plus_file(p_path.get_file())); } p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW); @@ -393,8 +207,8 @@ String OS_OSX::get_bundle_resource_dir() const { NSBundle *main = [NSBundle mainBundle]; if (main) { - NSString *resourcePath = [main resourcePath]; - ret.parse_utf8([resourcePath UTF8String]); + NSString *resource_path = [main resourcePath]; + ret.parse_utf8([resource_path UTF8String]); } return ret; } @@ -404,9 +218,9 @@ String OS_OSX::get_bundle_icon_path() const { NSBundle *main = [NSBundle mainBundle]; if (main) { - NSString *iconPath = [[main infoDictionary] objectForKey:@"CFBundleIconFile"]; - if (iconPath) { - ret.parse_utf8([iconPath UTF8String]); + NSString *icon_path = [[main infoDictionary] objectForKey:@"CFBundleIconFile"]; + if (icon_path) { + ret.parse_utf8([icon_path UTF8String]); } } return ret; @@ -449,9 +263,7 @@ String OS_OSX::get_system_dir(SystemDir p_dir, bool p_shared_storage) const { if (found) { NSArray *paths = NSSearchPathForDirectoriesInDomains(id, NSUserDomainMask, YES); if (paths && [paths count] >= 1) { - char *utfs = strdup([[paths firstObject] UTF8String]); - ret.parse_utf8(utfs); - free(utfs); + ret.parse_utf8([[paths firstObject] UTF8String]); } } @@ -475,12 +287,9 @@ String OS_OSX::get_locale() const { } String OS_OSX::get_executable_path() const { - int ret; - pid_t pid; char pathbuf[PROC_PIDPATHINFO_MAXSIZE]; - - pid = getpid(); - ret = proc_pidpath(pid, pathbuf, sizeof(pathbuf)); + int pid = getpid(); + pid_t ret = proc_pidpath(pid, pathbuf, sizeof(pathbuf)); if (ret <= 0) { return OS::get_executable_path(); } else { @@ -491,19 +300,7 @@ String OS_OSX::get_executable_path() const { } } -Error OS_OSX::create_instance(const List<String> &p_arguments, ProcessID *r_child_id) { - // If executable is bundled, always execute editor instances as an app bundle to ensure app window is registered and activated correctly. - NSString *nsappname = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"]; - if (nsappname != nil) { - String path; - path.parse_utf8([[[NSBundle mainBundle] bundlePath] UTF8String]); - return create_process(path, p_arguments, r_child_id); - } else { - return create_process(get_executable_path(), p_arguments, r_child_id); - } -} - -Error OS_OSX::create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id) { +Error OS_OSX::create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id, bool p_open_console) { if (@available(macOS 10.15, *)) { // Use NSWorkspace if path is an .app bundle. NSURL *url = [NSURL fileURLWithPath:@(p_path.utf8().get_data())]; @@ -532,7 +329,6 @@ Error OS_OSX::create_process(const String &p_path, const List<String> &p_argumen dispatch_semaphore_signal(lock); }]; dispatch_semaphore_wait(lock, dispatch_time(DISPATCH_TIME_NOW, 20000000000)); // 20 sec timeout, wait for app to launch. - dispatch_release(lock); if (err == OK) { if (r_child_id) { @@ -542,24 +338,73 @@ Error OS_OSX::create_process(const String &p_path, const List<String> &p_argumen return err; } else { - return OS_Unix::create_process(p_path, p_arguments, r_child_id); + return OS_Unix::create_process(p_path, p_arguments, r_child_id, p_open_console); } } else { - return OS_Unix::create_process(p_path, p_arguments, r_child_id); + return OS_Unix::create_process(p_path, p_arguments, r_child_id, p_open_console); } } -void OS_OSX::pre_wait_observer_cb(CFRunLoopObserverRef p_observer, CFRunLoopActivity p_activiy, void *p_context) { - // Prevent main loop from sleeping and redraw window during resize / modal popups. +Error OS_OSX::create_instance(const List<String> &p_arguments, ProcessID *r_child_id) { + // If executable is bundled, always execute editor instances as an app bundle to ensure app window is registered and activated correctly. + NSString *nsappname = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"]; + if (nsappname != nil) { + String path; + path.parse_utf8([[[NSBundle mainBundle] bundlePath] UTF8String]); + return create_process(path, p_arguments, r_child_id, false); + } else { + return create_process(get_executable_path(), p_arguments, r_child_id, false); + } +} - if (get_singleton()->get_main_loop()) { - Main::force_redraw(); - if (!Main::is_iterating()) { // Avoid cyclic loop. - Main::iteration(); +String OS_OSX::get_unique_id() const { + static String serial_number; + + if (serial_number.is_empty()) { + io_service_t platform_expert = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice")); + CFStringRef serial_number_cf_string = nullptr; + if (platform_expert) { + serial_number_cf_string = (CFStringRef)IORegistryEntryCreateCFProperty(platform_expert, CFSTR(kIOPlatformSerialNumberKey), kCFAllocatorDefault, 0); + IOObjectRelease(platform_expert); + } + + NSString *serial_number_ns_string = nil; + if (serial_number_cf_string) { + serial_number_ns_string = [NSString stringWithString:(__bridge NSString *)serial_number_cf_string]; + CFRelease(serial_number_cf_string); + } + + if (serial_number_ns_string) { + serial_number.parse_utf8([serial_number_ns_string UTF8String]); } } - CFRunLoopWakeUp(CFRunLoopGetCurrent()); // Prevent main loop from sleeping. + return serial_number; +} + +bool OS_OSX::_check_internal_feature_support(const String &p_feature) { + return p_feature == "pc"; +} + +void OS_OSX::disable_crash_handler() { + crash_handler.disable(); +} + +bool OS_OSX::is_disable_crash_handler() const { + return crash_handler.is_disabled(); +} + +Error OS_OSX::move_to_trash(const String &p_path) { + NSFileManager *fm = [NSFileManager defaultManager]; + NSURL *url = [NSURL fileURLWithPath:@(p_path.utf8().get_data())]; + NSError *err; + + if (![fm trashItemAtURL:url resultingItemURL:nil error:&err]) { + ERR_PRINT("trashItemAtURL error: " + String::utf8(err.localizedDescription.UTF8String)); + return FAILED; + } + + return OK; } void OS_OSX::run() { @@ -571,14 +416,11 @@ void OS_OSX::run() { main_loop->initialize(); - CFRunLoopObserverRef pre_wait_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, &pre_wait_observer_cb, nullptr); - CFRunLoopAddObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes); - bool quit = false; while (!force_quit && !quit) { @try { if (DisplayServer::get_singleton()) { - DisplayServer::get_singleton()->process_events(); // get rid of pending events + DisplayServer::get_singleton()->process_events(); // Get rid of pending events. } joypad_osx->process_joypads(); @@ -586,29 +428,13 @@ void OS_OSX::run() { quit = true; } } @catch (NSException *exception) { - ERR_PRINT("NSException: " + String([exception reason].UTF8String)); + ERR_PRINT("NSException: " + String::utf8([exception reason].UTF8String)); } }; - CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes); - CFRelease(pre_wait_observer); - main_loop->finalize(); } -Error OS_OSX::move_to_trash(const String &p_path) { - NSFileManager *fm = [NSFileManager defaultManager]; - NSURL *url = [NSURL fileURLWithPath:@(p_path.utf8().get_data())]; - NSError *err; - - if (![fm trashItemAtURL:url resultingItemURL:nil error:&err]) { - ERR_PRINT("trashItemAtURL error: " + String(err.localizedDescription.UTF8String)); - return FAILED; - } - - return OK; -} - OS_OSX::OS_OSX() { main_loop = nullptr; force_quit = false; @@ -623,17 +449,17 @@ OS_OSX::OS_OSX() { DisplayServerOSX::register_osx_driver(); - // Implicitly create shared NSApplication instance + // Implicitly create shared NSApplication instance. [GodotApplication sharedApplication]; - // In case we are unbundled, make us a proper UI application + // In case we are unbundled, make us a proper UI application. [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; // Menu bar setup must go between sharedApplication above and // finishLaunching below, in order to properly emulate the behavior - // of NSApplicationMain + // of NSApplicationMain. - NSMenu *main_menu = [[[NSMenu alloc] initWithTitle:@""] autorelease]; + NSMenu *main_menu = [[NSMenu alloc] initWithTitle:@""]; [NSApp setMainMenu:main_menu]; [NSApp finishLaunching]; @@ -641,7 +467,10 @@ OS_OSX::OS_OSX() { ERR_FAIL_COND(!delegate); [NSApp setDelegate:delegate]; - //process application:openFile: event + pre_wait_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, &pre_wait_observer_cb, nullptr); + CFRunLoopAddObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes); + + // Process application:openFile: event. while (true) { NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny @@ -659,14 +488,7 @@ OS_OSX::OS_OSX() { [NSApp activateIgnoringOtherApps:YES]; } -bool OS_OSX::_check_internal_feature_support(const String &p_feature) { - return p_feature == "pc"; -} - -void OS_OSX::disable_crash_handler() { - crash_handler.disable(); -} - -bool OS_OSX::is_disable_crash_handler() const { - return crash_handler.is_disabled(); +OS_OSX::~OS_OSX() { + CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes); + CFRelease(pre_wait_observer); } diff --git a/platform/osx/osx_terminal_logger.h b/platform/osx/osx_terminal_logger.h new file mode 100644 index 0000000000..8413509c4b --- /dev/null +++ b/platform/osx/osx_terminal_logger.h @@ -0,0 +1,44 @@ +/*************************************************************************/ +/* osx_terminal_logger.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef OSX_TERMINAL_LOGGER_H +#define OSX_TERMINAL_LOGGER_H + +#ifdef OSX_ENABLED + +#include "core/io/logger.h" + +class OSXTerminalLogger : public StdLogger { +public: + virtual void log_error(const char *p_function, const char *p_file, int p_line, const char *p_code, const char *p_rationale, bool p_editor_notify = false, ErrorType p_type = ERR_ERROR) override; +}; + +#endif // OSX_ENABLED +#endif // OSX_TERMINAL_LOGGER_H diff --git a/platform/osx/osx_terminal_logger.mm b/platform/osx/osx_terminal_logger.mm new file mode 100644 index 0000000000..c1dca111a7 --- /dev/null +++ b/platform/osx/osx_terminal_logger.mm @@ -0,0 +1,81 @@ +/*************************************************************************/ +/* osx_terminal_logger.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "osx_terminal_logger.h" + +#ifdef OSX_ENABLED + +#include <os/log.h> + +void OSXTerminalLogger::log_error(const char *p_function, const char *p_file, int p_line, const char *p_code, const char *p_rationale, bool p_editor_notify, ErrorType p_type) { + if (!should_log(true)) { + return; + } + + const char *err_details; + if (p_rationale && p_rationale[0]) + err_details = p_rationale; + else + err_details = p_code; + + switch (p_type) { + case ERR_WARNING: + os_log_info(OS_LOG_DEFAULT, + "WARNING: %{public}s\nat: %{public}s (%{public}s:%i)", + err_details, p_function, p_file, p_line); + logf_error("\E[1;33mWARNING:\E[0;93m %s\n", err_details); + logf_error("\E[0;90m at: %s (%s:%i)\E[0m\n", p_function, p_file, p_line); + break; + case ERR_SCRIPT: + os_log_error(OS_LOG_DEFAULT, + "SCRIPT ERROR: %{public}s\nat: %{public}s (%{public}s:%i)", + err_details, p_function, p_file, p_line); + logf_error("\E[1;35mSCRIPT ERROR:\E[0;95m %s\n", err_details); + logf_error("\E[0;90m at: %s (%s:%i)\E[0m\n", p_function, p_file, p_line); + break; + case ERR_SHADER: + os_log_error(OS_LOG_DEFAULT, + "SHADER ERROR: %{public}s\nat: %{public}s (%{public}s:%i)", + err_details, p_function, p_file, p_line); + logf_error("\E[1;36mSHADER ERROR:\E[0;96m %s\n", err_details); + logf_error("\E[0;90m at: %s (%s:%i)\E[0m\n", p_function, p_file, p_line); + break; + case ERR_ERROR: + default: + os_log_error(OS_LOG_DEFAULT, + "ERROR: %{public}s\nat: %{public}s (%{public}s:%i)", + err_details, p_function, p_file, p_line); + logf_error("\E[1;31mERROR:\E[0;91m %s\n", err_details); + logf_error("\E[0;90m at: %s (%s:%i)\E[0m\n", p_function, p_file, p_line); + break; + } +} + +#endif // OSX_ENABLED diff --git a/platform/osx/platform_config.h b/platform/osx/platform_config.h index 7bfa466b97..e114606b82 100644 --- a/platform/osx/platform_config.h +++ b/platform/osx/platform_config.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/platform/osx/vulkan_context_osx.h b/platform/osx/vulkan_context_osx.h index 22d43688a3..b78b4eb141 100644 --- a/platform/osx/vulkan_context_osx.h +++ b/platform/osx/vulkan_context_osx.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/platform/osx/vulkan_context_osx.mm b/platform/osx/vulkan_context_osx.mm index 36c02c2497..bdabc24c28 100644 --- a/platform/osx/vulkan_context_osx.mm +++ b/platform/osx/vulkan_context_osx.mm @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -44,7 +44,7 @@ Error VulkanContextOSX::window_create(DisplayServer::WindowID p_window_id, Displ createInfo.sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK; createInfo.pNext = nullptr; createInfo.flags = 0; - createInfo.pView = p_window; + createInfo.pView = (__bridge const void *)p_window; VkSurfaceKHR surface; VkResult err = vkCreateMacOSSurfaceMVK(get_instance(), &createInfo, nullptr, &surface); |