2171 lines
74 KiB
Plaintext
2171 lines
74 KiB
Plaintext
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
#include "GHOST_SystemCocoa.hh"
|
|
|
|
#include "GHOST_DisplayManagerCocoa.hh"
|
|
#include "GHOST_EventButton.hh"
|
|
#include "GHOST_EventCursor.hh"
|
|
#include "GHOST_EventDragnDrop.hh"
|
|
#include "GHOST_EventKey.hh"
|
|
#include "GHOST_EventString.hh"
|
|
#include "GHOST_EventTrackpad.hh"
|
|
#include "GHOST_EventWheel.hh"
|
|
#include "GHOST_TimerManager.hh"
|
|
#include "GHOST_TimerTask.hh"
|
|
#include "GHOST_WindowCocoa.hh"
|
|
#include "GHOST_WindowManager.hh"
|
|
|
|
/* Don't generate OpenGL deprecation warning. This is a known thing, and is not something easily
|
|
* solvable in a short term. */
|
|
#ifdef __clang__
|
|
# pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
#endif
|
|
|
|
#ifdef WITH_METAL_BACKEND
|
|
# include "GHOST_ContextCGL.hh"
|
|
#endif
|
|
|
|
#ifdef WITH_VULKAN_BACKEND
|
|
# include "GHOST_ContextVK.hh"
|
|
#endif
|
|
|
|
#ifdef WITH_INPUT_NDOF
|
|
# include "GHOST_NDOFManagerCocoa.hh"
|
|
#endif
|
|
|
|
#include "AssertMacros.h"
|
|
|
|
#import <Cocoa/Cocoa.h>
|
|
|
|
/* For the currently not ported to Cocoa keyboard layout functions (64bit & 10.6 compatible) */
|
|
#include <Carbon/Carbon.h>
|
|
|
|
#include <sys/sysctl.h>
|
|
#include <sys/time.h>
|
|
#include <sys/types.h>
|
|
|
|
#include <mach/mach_time.h>
|
|
|
|
#pragma mark KeyMap, mouse converters
|
|
|
|
static GHOST_TButton convertButton(int button)
|
|
{
|
|
switch (button) {
|
|
case 0:
|
|
return GHOST_kButtonMaskLeft;
|
|
case 1:
|
|
return GHOST_kButtonMaskRight;
|
|
case 2:
|
|
return GHOST_kButtonMaskMiddle;
|
|
case 3:
|
|
return GHOST_kButtonMaskButton4;
|
|
case 4:
|
|
return GHOST_kButtonMaskButton5;
|
|
case 5:
|
|
return GHOST_kButtonMaskButton6;
|
|
case 6:
|
|
return GHOST_kButtonMaskButton7;
|
|
default:
|
|
return GHOST_kButtonMaskLeft;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts Mac raw-key codes (same for Cocoa & Carbon)
|
|
* into GHOST key codes
|
|
* \param rawCode: The raw physical key code
|
|
* \param recvChar: the character ignoring modifiers (except for shift)
|
|
* \return Ghost key code
|
|
*/
|
|
static GHOST_TKey convertKey(int rawCode, unichar recvChar)
|
|
{
|
|
// printf("\nrecvchar %c 0x%x",recvChar,recvChar);
|
|
switch (rawCode) {
|
|
/* Physical key-codes: (not used due to map changes in international keyboards). */
|
|
#if 0
|
|
case kVK_ANSI_A: return GHOST_kKeyA;
|
|
case kVK_ANSI_B: return GHOST_kKeyB;
|
|
case kVK_ANSI_C: return GHOST_kKeyC;
|
|
case kVK_ANSI_D: return GHOST_kKeyD;
|
|
case kVK_ANSI_E: return GHOST_kKeyE;
|
|
case kVK_ANSI_F: return GHOST_kKeyF;
|
|
case kVK_ANSI_G: return GHOST_kKeyG;
|
|
case kVK_ANSI_H: return GHOST_kKeyH;
|
|
case kVK_ANSI_I: return GHOST_kKeyI;
|
|
case kVK_ANSI_J: return GHOST_kKeyJ;
|
|
case kVK_ANSI_K: return GHOST_kKeyK;
|
|
case kVK_ANSI_L: return GHOST_kKeyL;
|
|
case kVK_ANSI_M: return GHOST_kKeyM;
|
|
case kVK_ANSI_N: return GHOST_kKeyN;
|
|
case kVK_ANSI_O: return GHOST_kKeyO;
|
|
case kVK_ANSI_P: return GHOST_kKeyP;
|
|
case kVK_ANSI_Q: return GHOST_kKeyQ;
|
|
case kVK_ANSI_R: return GHOST_kKeyR;
|
|
case kVK_ANSI_S: return GHOST_kKeyS;
|
|
case kVK_ANSI_T: return GHOST_kKeyT;
|
|
case kVK_ANSI_U: return GHOST_kKeyU;
|
|
case kVK_ANSI_V: return GHOST_kKeyV;
|
|
case kVK_ANSI_W: return GHOST_kKeyW;
|
|
case kVK_ANSI_X: return GHOST_kKeyX;
|
|
case kVK_ANSI_Y: return GHOST_kKeyY;
|
|
case kVK_ANSI_Z: return GHOST_kKeyZ;
|
|
#endif
|
|
/* Numbers keys: mapped to handle some international keyboard (e.g. French). */
|
|
case kVK_ANSI_1:
|
|
return GHOST_kKey1;
|
|
case kVK_ANSI_2:
|
|
return GHOST_kKey2;
|
|
case kVK_ANSI_3:
|
|
return GHOST_kKey3;
|
|
case kVK_ANSI_4:
|
|
return GHOST_kKey4;
|
|
case kVK_ANSI_5:
|
|
return GHOST_kKey5;
|
|
case kVK_ANSI_6:
|
|
return GHOST_kKey6;
|
|
case kVK_ANSI_7:
|
|
return GHOST_kKey7;
|
|
case kVK_ANSI_8:
|
|
return GHOST_kKey8;
|
|
case kVK_ANSI_9:
|
|
return GHOST_kKey9;
|
|
case kVK_ANSI_0:
|
|
return GHOST_kKey0;
|
|
|
|
case kVK_ANSI_Keypad0:
|
|
return GHOST_kKeyNumpad0;
|
|
case kVK_ANSI_Keypad1:
|
|
return GHOST_kKeyNumpad1;
|
|
case kVK_ANSI_Keypad2:
|
|
return GHOST_kKeyNumpad2;
|
|
case kVK_ANSI_Keypad3:
|
|
return GHOST_kKeyNumpad3;
|
|
case kVK_ANSI_Keypad4:
|
|
return GHOST_kKeyNumpad4;
|
|
case kVK_ANSI_Keypad5:
|
|
return GHOST_kKeyNumpad5;
|
|
case kVK_ANSI_Keypad6:
|
|
return GHOST_kKeyNumpad6;
|
|
case kVK_ANSI_Keypad7:
|
|
return GHOST_kKeyNumpad7;
|
|
case kVK_ANSI_Keypad8:
|
|
return GHOST_kKeyNumpad8;
|
|
case kVK_ANSI_Keypad9:
|
|
return GHOST_kKeyNumpad9;
|
|
case kVK_ANSI_KeypadDecimal:
|
|
return GHOST_kKeyNumpadPeriod;
|
|
case kVK_ANSI_KeypadEnter:
|
|
return GHOST_kKeyNumpadEnter;
|
|
case kVK_ANSI_KeypadPlus:
|
|
return GHOST_kKeyNumpadPlus;
|
|
case kVK_ANSI_KeypadMinus:
|
|
return GHOST_kKeyNumpadMinus;
|
|
case kVK_ANSI_KeypadMultiply:
|
|
return GHOST_kKeyNumpadAsterisk;
|
|
case kVK_ANSI_KeypadDivide:
|
|
return GHOST_kKeyNumpadSlash;
|
|
case kVK_ANSI_KeypadClear:
|
|
return GHOST_kKeyUnknown;
|
|
|
|
case kVK_F1:
|
|
return GHOST_kKeyF1;
|
|
case kVK_F2:
|
|
return GHOST_kKeyF2;
|
|
case kVK_F3:
|
|
return GHOST_kKeyF3;
|
|
case kVK_F4:
|
|
return GHOST_kKeyF4;
|
|
case kVK_F5:
|
|
return GHOST_kKeyF5;
|
|
case kVK_F6:
|
|
return GHOST_kKeyF6;
|
|
case kVK_F7:
|
|
return GHOST_kKeyF7;
|
|
case kVK_F8:
|
|
return GHOST_kKeyF8;
|
|
case kVK_F9:
|
|
return GHOST_kKeyF9;
|
|
case kVK_F10:
|
|
return GHOST_kKeyF10;
|
|
case kVK_F11:
|
|
return GHOST_kKeyF11;
|
|
case kVK_F12:
|
|
return GHOST_kKeyF12;
|
|
case kVK_F13:
|
|
return GHOST_kKeyF13;
|
|
case kVK_F14:
|
|
return GHOST_kKeyF14;
|
|
case kVK_F15:
|
|
return GHOST_kKeyF15;
|
|
case kVK_F16:
|
|
return GHOST_kKeyF16;
|
|
case kVK_F17:
|
|
return GHOST_kKeyF17;
|
|
case kVK_F18:
|
|
return GHOST_kKeyF18;
|
|
case kVK_F19:
|
|
return GHOST_kKeyF19;
|
|
case kVK_F20:
|
|
return GHOST_kKeyF20;
|
|
|
|
case kVK_UpArrow:
|
|
return GHOST_kKeyUpArrow;
|
|
case kVK_DownArrow:
|
|
return GHOST_kKeyDownArrow;
|
|
case kVK_LeftArrow:
|
|
return GHOST_kKeyLeftArrow;
|
|
case kVK_RightArrow:
|
|
return GHOST_kKeyRightArrow;
|
|
|
|
case kVK_Return:
|
|
return GHOST_kKeyEnter;
|
|
case kVK_Delete:
|
|
return GHOST_kKeyBackSpace;
|
|
case kVK_ForwardDelete:
|
|
return GHOST_kKeyDelete;
|
|
case kVK_Escape:
|
|
return GHOST_kKeyEsc;
|
|
case kVK_Tab:
|
|
return GHOST_kKeyTab;
|
|
case kVK_Space:
|
|
return GHOST_kKeySpace;
|
|
|
|
case kVK_Home:
|
|
return GHOST_kKeyHome;
|
|
case kVK_End:
|
|
return GHOST_kKeyEnd;
|
|
case kVK_PageUp:
|
|
return GHOST_kKeyUpPage;
|
|
case kVK_PageDown:
|
|
return GHOST_kKeyDownPage;
|
|
#if 0
|
|
/* These constants with "ANSI" in the name are labeled according to the key position on an
|
|
* ANSI-standard US keyboard. Therefore they may not match the physical key label on other
|
|
* keyboard layouts. */
|
|
case kVK_ANSI_Minus: return GHOST_kKeyMinus;
|
|
case kVK_ANSI_Equal: return GHOST_kKeyEqual;
|
|
case kVK_ANSI_Comma: return GHOST_kKeyComma;
|
|
case kVK_ANSI_Period: return GHOST_kKeyPeriod;
|
|
case kVK_ANSI_Slash: return GHOST_kKeySlash;
|
|
case kVK_ANSI_Semicolon: return GHOST_kKeySemicolon;
|
|
case kVK_ANSI_Quote: return GHOST_kKeyQuote;
|
|
case kVK_ANSI_Backslash: return GHOST_kKeyBackslash;
|
|
case kVK_ANSI_LeftBracket: return GHOST_kKeyLeftBracket;
|
|
case kVK_ANSI_RightBracket: return GHOST_kKeyRightBracket;
|
|
case kVK_ANSI_Grave: return GHOST_kKeyAccentGrave;
|
|
case kVK_ISO_Section: return GHOST_kKeyUnknown;
|
|
#endif
|
|
case kVK_VolumeUp:
|
|
case kVK_VolumeDown:
|
|
case kVK_Mute:
|
|
return GHOST_kKeyUnknown;
|
|
|
|
default: {
|
|
/* Alphanumerical or punctuation key that is remappable in international keyboards. */
|
|
if ((recvChar >= 'A') && (recvChar <= 'Z')) {
|
|
return (GHOST_TKey)(recvChar - 'A' + GHOST_kKeyA);
|
|
}
|
|
else if ((recvChar >= 'a') && (recvChar <= 'z')) {
|
|
return (GHOST_TKey)(recvChar - 'a' + GHOST_kKeyA);
|
|
}
|
|
else {
|
|
/* Leopard and Snow Leopard 64bit compatible API. */
|
|
CFDataRef uchrHandle; /* The keyboard layout. */
|
|
TISInputSourceRef kbdTISHandle;
|
|
|
|
kbdTISHandle = TISCopyCurrentKeyboardLayoutInputSource();
|
|
uchrHandle = (CFDataRef)TISGetInputSourceProperty(kbdTISHandle,
|
|
kTISPropertyUnicodeKeyLayoutData);
|
|
CFRelease(kbdTISHandle);
|
|
|
|
/* Get actual character value of the "remappable" keys in international keyboards,
|
|
* if keyboard layout is not correctly reported (e.g. some non Apple keyboards in Tiger),
|
|
* then fallback on using the received #charactersIgnoringModifiers. */
|
|
if (uchrHandle) {
|
|
UInt32 deadKeyState = 0;
|
|
UniCharCount actualStrLength = 0;
|
|
|
|
UCKeyTranslate((UCKeyboardLayout *)CFDataGetBytePtr(uchrHandle),
|
|
rawCode,
|
|
kUCKeyActionDown,
|
|
0,
|
|
LMGetKbdType(),
|
|
kUCKeyTranslateNoDeadKeysMask,
|
|
&deadKeyState,
|
|
1,
|
|
&actualStrLength,
|
|
&recvChar);
|
|
}
|
|
|
|
switch (recvChar) {
|
|
case '-':
|
|
return GHOST_kKeyMinus;
|
|
case '+':
|
|
return GHOST_kKeyPlus;
|
|
case '=':
|
|
return GHOST_kKeyEqual;
|
|
case ',':
|
|
return GHOST_kKeyComma;
|
|
case '.':
|
|
return GHOST_kKeyPeriod;
|
|
case '/':
|
|
return GHOST_kKeySlash;
|
|
case ';':
|
|
return GHOST_kKeySemicolon;
|
|
case '\'':
|
|
return GHOST_kKeyQuote;
|
|
case '\\':
|
|
return GHOST_kKeyBackslash;
|
|
case '[':
|
|
return GHOST_kKeyLeftBracket;
|
|
case ']':
|
|
return GHOST_kKeyRightBracket;
|
|
case '`':
|
|
case '<': /* The position of '`' is equivalent to this symbol in the French layout. */
|
|
return GHOST_kKeyAccentGrave;
|
|
default:
|
|
return GHOST_kKeyUnknown;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return GHOST_kKeyUnknown;
|
|
}
|
|
|
|
#pragma mark Utility functions
|
|
|
|
#define FIRSTFILEBUFLG 512
|
|
static bool g_hasFirstFile = false;
|
|
static char g_firstFileBuf[FIRSTFILEBUFLG];
|
|
|
|
/* TODO: Need to investigate this.
|
|
* Function called too early in creator.c to have g_hasFirstFile == true */
|
|
extern "C" int GHOST_HACK_getFirstFile(char buf[FIRSTFILEBUFLG])
|
|
{
|
|
if (g_hasFirstFile) {
|
|
memcpy(buf, g_firstFileBuf, FIRSTFILEBUFLG);
|
|
buf[FIRSTFILEBUFLG - 1] = '\0';
|
|
return 1;
|
|
}
|
|
else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
#pragma mark Cocoa objects
|
|
|
|
/**
|
|
* CocoaAppDelegate
|
|
* ObjC object to capture applicationShouldTerminate, and send quit event
|
|
*/
|
|
@interface CocoaAppDelegate : NSObject <NSApplicationDelegate>
|
|
{
|
|
|
|
GHOST_SystemCocoa *systemCocoa;
|
|
}
|
|
|
|
- (id)init;
|
|
- (void)dealloc;
|
|
- (void)setSystemCocoa:(GHOST_SystemCocoa *)sysCocoa;
|
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
|
|
- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename;
|
|
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender;
|
|
- (void)applicationWillTerminate:(NSNotification *)aNotification;
|
|
- (void)applicationWillBecomeActive:(NSNotification *)aNotification;
|
|
- (void)toggleFullScreen:(NSNotification *)notification;
|
|
- (void)windowWillClose:(NSNotification *)notification;
|
|
|
|
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app;
|
|
@end
|
|
|
|
@implementation CocoaAppDelegate : NSObject
|
|
- (id)init
|
|
{
|
|
self = [super init];
|
|
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
|
[center addObserver:self
|
|
selector:@selector(windowWillClose:)
|
|
name:NSWindowWillCloseNotification
|
|
object:nil];
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
|
[center removeObserver:self name:NSWindowWillCloseNotification object:nil];
|
|
[super dealloc];
|
|
}
|
|
|
|
- (void)setSystemCocoa:(GHOST_SystemCocoa *)sysCocoa
|
|
{
|
|
systemCocoa = sysCocoa;
|
|
}
|
|
|
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
|
|
{
|
|
if (systemCocoa->m_windowFocus) {
|
|
// Raise application to front, convenient when starting from the terminal
|
|
// and important for launching the animation player. we call this after the
|
|
// application finishes launching, as doing it earlier can make us end up
|
|
// with a frontmost window but an inactive application.
|
|
[NSApp activateIgnoringOtherApps:YES];
|
|
}
|
|
|
|
[NSEvent setMouseCoalescingEnabled:NO];
|
|
}
|
|
|
|
- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
|
|
{
|
|
return systemCocoa->handleOpenDocumentRequest(filename);
|
|
}
|
|
|
|
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
|
|
{
|
|
/* TODO: implement graceful termination through Cocoa mechanism
|
|
* to avoid session log off to be canceled. */
|
|
/* Note that Cmd+Q is already handled by key-handler. */
|
|
systemCocoa->handleQuitRequest();
|
|
return NSTerminateCancel;
|
|
}
|
|
|
|
// To avoid canceling a log off process, we must use Cocoa termination process
|
|
// And this function is the only chance to perform clean up
|
|
// So WM_exit needs to be called directly, as the event loop will never run before termination
|
|
- (void)applicationWillTerminate:(NSNotification *)aNotification
|
|
{
|
|
#if 0
|
|
WM_exit(C, EXIT_SUCCESS);
|
|
#endif
|
|
}
|
|
|
|
- (void)applicationWillBecomeActive:(NSNotification *)aNotification
|
|
{
|
|
systemCocoa->handleApplicationBecomeActiveEvent();
|
|
}
|
|
|
|
- (void)toggleFullScreen:(NSNotification *)notification
|
|
{
|
|
}
|
|
|
|
// The purpose of this function is to make sure closing "About" window does not
|
|
// leave Blender with no key windows. This is needed due to a custom event loop
|
|
// nature of the application: for some reason only using [NSApp run] will ensure
|
|
// correct behavior in this case.
|
|
//
|
|
// This is similar to an issue solved in SDL:
|
|
// https://bugzilla.libsdl.org/show_bug.cgi?id=1825
|
|
//
|
|
// Our solution is different, since we want Blender to keep track of what is
|
|
// the key window during normal operation. In order to do so we exploit the
|
|
// fact that "About" window is never in the orderedWindows array: we only force
|
|
// key window from here if the closing one is not in the orderedWindows. This
|
|
// saves lack of key windows when closing "About", but does not interfere with
|
|
// Blender's window manager when closing Blender's windows.
|
|
//
|
|
// NOTE: It also receives notifiers when menus are closed on macOS 14.
|
|
// Presumably it considers menus to be windows.
|
|
- (void)windowWillClose:(NSNotification *)notification
|
|
{
|
|
NSWindow *closing_window = (NSWindow *)[notification object];
|
|
|
|
if (![closing_window isKeyWindow]) {
|
|
/* If the window wasn't key then its either none of the windows are key or another window
|
|
* is a key. The former situation is a bit strange, but probably forcing a key window is not
|
|
* something desirable. The latter situation is when we definitely do not want to change the
|
|
* key window.
|
|
*
|
|
* Ignoring non-key windows also avoids the code which ensures ordering below from running
|
|
* when the notifier is received for menus on macOS 14. */
|
|
return;
|
|
}
|
|
|
|
NSInteger index = [[NSApp orderedWindows] indexOfObject:closing_window];
|
|
if (index != NSNotFound) {
|
|
return;
|
|
}
|
|
// Find first suitable window from the current space.
|
|
for (NSWindow *current_window in [NSApp orderedWindows]) {
|
|
if (current_window == closing_window) {
|
|
continue;
|
|
}
|
|
if ([current_window isOnActiveSpace] && [current_window canBecomeKeyWindow]) {
|
|
[current_window makeKeyAndOrderFront:nil];
|
|
return;
|
|
}
|
|
}
|
|
// If that didn't find any windows, we try to find any suitable window of
|
|
// the application.
|
|
for (NSNumber *window_number in [NSWindow windowNumbersWithOptions:0]) {
|
|
NSWindow *current_window = [NSApp windowWithWindowNumber:[window_number integerValue]];
|
|
if (current_window == closing_window) {
|
|
continue;
|
|
}
|
|
if ([current_window canBecomeKeyWindow]) {
|
|
[current_window makeKeyAndOrderFront:nil];
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Explicitly opt-in to the secure coding for the restorable state.
|
|
*
|
|
* This is something that only has affect on macOS 12+, and is implicitly
|
|
* enabled on macOS 14.
|
|
*
|
|
* For the details see
|
|
* https://sector7.computest.nl/post/2022-08-process-injection-breaking-all-macos-security-layers-with-a-single-vulnerability/
|
|
*/
|
|
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark initialization/finalization
|
|
|
|
GHOST_SystemCocoa::GHOST_SystemCocoa()
|
|
{
|
|
int mib[2];
|
|
struct timeval boottime;
|
|
size_t len;
|
|
char *rstring = nullptr;
|
|
|
|
m_modifierMask = 0;
|
|
m_outsideLoopEventProcessed = false;
|
|
m_needDelayedApplicationBecomeActiveEventProcessing = false;
|
|
m_displayManager = new GHOST_DisplayManagerCocoa();
|
|
GHOST_ASSERT(m_displayManager, "GHOST_SystemCocoa::GHOST_SystemCocoa(): m_displayManager==0\n");
|
|
m_displayManager->initialize();
|
|
|
|
// NSEvent timeStamp is given in system uptime, state start date is boot time
|
|
mib[0] = CTL_KERN;
|
|
mib[1] = KERN_BOOTTIME;
|
|
len = sizeof(struct timeval);
|
|
|
|
sysctl(mib, 2, &boottime, &len, nullptr, 0);
|
|
m_start_time = ((boottime.tv_sec * 1000) + (boottime.tv_usec / 1000));
|
|
|
|
/* Detect multi-touch trackpad. */
|
|
mib[0] = CTL_HW;
|
|
mib[1] = HW_MODEL;
|
|
sysctl(mib, 2, nullptr, &len, nullptr, 0);
|
|
rstring = (char *)malloc(len);
|
|
sysctl(mib, 2, rstring, &len, nullptr, 0);
|
|
|
|
free(rstring);
|
|
rstring = nullptr;
|
|
|
|
m_ignoreWindowSizedMessages = false;
|
|
m_ignoreMomentumScroll = false;
|
|
m_multiTouchScroll = false;
|
|
m_last_warp_timestamp = 0;
|
|
}
|
|
|
|
GHOST_SystemCocoa::~GHOST_SystemCocoa() {}
|
|
|
|
GHOST_TSuccess GHOST_SystemCocoa::init()
|
|
{
|
|
GHOST_TSuccess success = GHOST_System::init();
|
|
if (success) {
|
|
|
|
#ifdef WITH_INPUT_NDOF
|
|
m_ndofManager = new GHOST_NDOFManagerCocoa(*this);
|
|
#endif
|
|
|
|
// ProcessSerialNumber psn;
|
|
|
|
// Carbon stuff to move window & menu to foreground
|
|
#if 0
|
|
if (!GetCurrentProcess(&psn)) {
|
|
TransformProcessType(&psn, kProcessTransformToForegroundApplication);
|
|
SetFrontProcess(&psn);
|
|
}
|
|
#endif
|
|
|
|
@autoreleasepool {
|
|
[NSApplication sharedApplication]; // initializes NSApp
|
|
|
|
if ([NSApp mainMenu] == nil) {
|
|
NSMenu *mainMenubar = [[NSMenu alloc] init];
|
|
NSMenuItem *menuItem;
|
|
NSMenu *windowMenu;
|
|
NSMenu *appMenu;
|
|
|
|
// Create the application menu
|
|
appMenu = [[NSMenu alloc] initWithTitle:@"Blender"];
|
|
|
|
[appMenu addItemWithTitle:@"About Blender"
|
|
action:@selector(orderFrontStandardAboutPanel:)
|
|
keyEquivalent:@""];
|
|
[appMenu addItem:[NSMenuItem separatorItem]];
|
|
|
|
menuItem = [appMenu addItemWithTitle:@"Hide Blender"
|
|
action:@selector(hide:)
|
|
keyEquivalent:@"h"];
|
|
[menuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand];
|
|
|
|
menuItem = [appMenu addItemWithTitle:@"Hide Others"
|
|
action:@selector(hideOtherApplications:)
|
|
keyEquivalent:@"h"];
|
|
[menuItem
|
|
setKeyEquivalentModifierMask:(NSEventModifierFlagOption | NSEventModifierFlagCommand)];
|
|
|
|
[appMenu addItemWithTitle:@"Show All"
|
|
action:@selector(unhideAllApplications:)
|
|
keyEquivalent:@""];
|
|
|
|
menuItem = [appMenu addItemWithTitle:@"Quit Blender"
|
|
action:@selector(terminate:)
|
|
keyEquivalent:@"q"];
|
|
[menuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand];
|
|
|
|
menuItem = [[NSMenuItem alloc] init];
|
|
[menuItem setSubmenu:appMenu];
|
|
|
|
[mainMenubar addItem:menuItem];
|
|
[menuItem release];
|
|
[NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu]; // Needed for 10.5
|
|
[appMenu release];
|
|
|
|
// Create the window menu
|
|
windowMenu = [[NSMenu alloc] initWithTitle:@"Window"];
|
|
|
|
menuItem = [windowMenu addItemWithTitle:@"Minimize"
|
|
action:@selector(performMiniaturize:)
|
|
keyEquivalent:@"m"];
|
|
[menuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand];
|
|
|
|
[windowMenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""];
|
|
|
|
menuItem = [windowMenu addItemWithTitle:@"Enter Full Screen"
|
|
action:@selector(toggleFullScreen:)
|
|
keyEquivalent:@"f"];
|
|
[menuItem
|
|
setKeyEquivalentModifierMask:NSEventModifierFlagControl | NSEventModifierFlagCommand];
|
|
|
|
menuItem = [windowMenu addItemWithTitle:@"Close"
|
|
action:@selector(performClose:)
|
|
keyEquivalent:@"w"];
|
|
[menuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand];
|
|
|
|
menuItem = [[NSMenuItem alloc] init];
|
|
[menuItem setSubmenu:windowMenu];
|
|
|
|
[mainMenubar addItem:menuItem];
|
|
[menuItem release];
|
|
|
|
[NSApp setMainMenu:mainMenubar];
|
|
[NSApp setWindowsMenu:windowMenu];
|
|
[windowMenu release];
|
|
}
|
|
|
|
if ([NSApp delegate] == nil) {
|
|
CocoaAppDelegate *appDelegate = [[CocoaAppDelegate alloc] init];
|
|
[appDelegate setSystemCocoa:this];
|
|
[NSApp setDelegate:appDelegate];
|
|
}
|
|
|
|
// AppKit provides automatic window tabbing. Blender is a single-tabbed application
|
|
// without a macOS tab bar, and should explicitly opt-out of this. This is also
|
|
// controlled by the macOS user default #NSWindowTabbingEnabled.
|
|
NSWindow.allowsAutomaticWindowTabbing = NO;
|
|
|
|
[NSApp finishLaunching];
|
|
}
|
|
}
|
|
return success;
|
|
}
|
|
|
|
#pragma mark window management
|
|
|
|
uint64_t GHOST_SystemCocoa::getMilliSeconds() const
|
|
{
|
|
// Cocoa equivalent exists in 10.6 ([[NSProcessInfo processInfo] systemUptime])
|
|
struct timeval currentTime;
|
|
|
|
gettimeofday(¤tTime, nullptr);
|
|
|
|
// Return timestamp of system uptime
|
|
|
|
return ((currentTime.tv_sec * 1000) + (currentTime.tv_usec / 1000) - m_start_time);
|
|
}
|
|
|
|
uint8_t GHOST_SystemCocoa::getNumDisplays() const
|
|
{
|
|
// Note that OS X supports monitor hot plug
|
|
// We do not support multiple monitors at the moment
|
|
@autoreleasepool {
|
|
return NSScreen.screens.count;
|
|
}
|
|
}
|
|
|
|
void GHOST_SystemCocoa::getMainDisplayDimensions(uint32_t &width, uint32_t &height) const
|
|
{
|
|
@autoreleasepool {
|
|
// Get visible frame, that is frame excluding dock and top menu bar
|
|
NSRect frame = [[NSScreen mainScreen] visibleFrame];
|
|
|
|
// Returns max window contents (excluding title bar...)
|
|
NSRect contentRect = [NSWindow
|
|
contentRectForFrameRect:frame
|
|
styleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
|
|
NSWindowStyleMaskMiniaturizable)];
|
|
|
|
width = contentRect.size.width;
|
|
height = contentRect.size.height;
|
|
}
|
|
}
|
|
|
|
void GHOST_SystemCocoa::getAllDisplayDimensions(uint32_t &width, uint32_t &height) const
|
|
{
|
|
/* TODO! */
|
|
getMainDisplayDimensions(width, height);
|
|
}
|
|
|
|
GHOST_IWindow *GHOST_SystemCocoa::createWindow(const char *title,
|
|
int32_t left,
|
|
int32_t top,
|
|
uint32_t width,
|
|
uint32_t height,
|
|
GHOST_TWindowState state,
|
|
GHOST_GPUSettings gpuSettings,
|
|
const bool /*exclusive*/,
|
|
const bool is_dialog,
|
|
const GHOST_IWindow *parentWindow)
|
|
{
|
|
GHOST_IWindow *window = nullptr;
|
|
@autoreleasepool {
|
|
|
|
// Get the available rect for including window contents
|
|
NSRect frame = [[NSScreen mainScreen] visibleFrame];
|
|
NSRect contentRect = [NSWindow
|
|
contentRectForFrameRect:frame
|
|
styleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
|
|
NSWindowStyleMaskMiniaturizable)];
|
|
|
|
int32_t bottom = (contentRect.size.height - 1) - height - top;
|
|
|
|
// Ensures window top left is inside this available rect
|
|
left = left > contentRect.origin.x ? left : contentRect.origin.x;
|
|
// Add contentRect.origin.y to respect docksize
|
|
bottom = bottom > contentRect.origin.y ? bottom + contentRect.origin.y : contentRect.origin.y;
|
|
|
|
window = new GHOST_WindowCocoa(this,
|
|
title,
|
|
left,
|
|
bottom,
|
|
width,
|
|
height,
|
|
state,
|
|
gpuSettings.context_type,
|
|
gpuSettings.flags & GHOST_gpuStereoVisual,
|
|
gpuSettings.flags & GHOST_gpuDebugContext,
|
|
is_dialog,
|
|
(GHOST_WindowCocoa *)parentWindow);
|
|
|
|
if (window->getValid()) {
|
|
// Store the pointer to the window
|
|
GHOST_ASSERT(m_windowManager, "m_windowManager not initialized");
|
|
m_windowManager->addWindow(window);
|
|
m_windowManager->setActiveWindow(window);
|
|
/* Need to tell window manager the new window is the active one
|
|
* (Cocoa does not send the event activate upon window creation). */
|
|
pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowActivate, window));
|
|
pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowSize, window));
|
|
}
|
|
else {
|
|
GHOST_PRINT("GHOST_SystemCocoa::createWindow(): window invalid\n");
|
|
delete window;
|
|
window = nullptr;
|
|
}
|
|
}
|
|
return window;
|
|
}
|
|
|
|
/**
|
|
* Create a new off-screen context.
|
|
* Never explicitly delete the context, use #disposeContext() instead.
|
|
* \return The new context (or 0 if creation failed).
|
|
*/
|
|
GHOST_IContext *GHOST_SystemCocoa::createOffscreenContext(GHOST_GPUSettings gpuSettings)
|
|
{
|
|
const bool debug_context = (gpuSettings.flags & GHOST_gpuDebugContext) != 0;
|
|
|
|
switch (gpuSettings.context_type) {
|
|
#ifdef WITH_VULKAN_BACKEND
|
|
case GHOST_kDrawingContextTypeVulkan: {
|
|
GHOST_Context *context = new GHOST_ContextVK(false, nullptr, 1, 2, debug_context);
|
|
if (context->initializeDrawingContext()) {
|
|
return context;
|
|
}
|
|
delete context;
|
|
return nullptr;
|
|
}
|
|
#endif
|
|
|
|
#ifdef WITH_METAL_BACKEND
|
|
case GHOST_kDrawingContextTypeMetal: {
|
|
/* TODO(fclem): Remove OpenGL support and rename context to ContextMTL */
|
|
GHOST_Context *context = new GHOST_ContextCGL(false, nullptr, nullptr, debug_context);
|
|
if (context->initializeDrawingContext()) {
|
|
return context;
|
|
}
|
|
delete context;
|
|
return nullptr;
|
|
}
|
|
#endif
|
|
|
|
default:
|
|
/* Unsupported backend. */
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dispose of a context.
|
|
* \param context: Pointer to the context to be disposed.
|
|
* \return Indication of success.
|
|
*/
|
|
GHOST_TSuccess GHOST_SystemCocoa::disposeContext(GHOST_IContext *context)
|
|
{
|
|
delete context;
|
|
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_IWindow *GHOST_SystemCocoa::getWindowUnderCursor(int32_t x, int32_t y)
|
|
{
|
|
NSPoint scr_co = NSMakePoint(x, y);
|
|
|
|
int windowNumberAtPoint = [NSWindow windowNumberAtPoint:scr_co belowWindowWithWindowNumber:0];
|
|
NSWindow *nswindow = [NSApp windowWithWindowNumber:windowNumberAtPoint];
|
|
|
|
if (nswindow == nil) {
|
|
return nil;
|
|
}
|
|
|
|
return m_windowManager->getWindowAssociatedWithOSWindow((const void *)nswindow);
|
|
}
|
|
|
|
/**
|
|
* \note returns coordinates in Cocoa screen coordinates.
|
|
*/
|
|
GHOST_TSuccess GHOST_SystemCocoa::getCursorPosition(int32_t &x, int32_t &y) const
|
|
{
|
|
NSPoint mouseLoc = [NSEvent mouseLocation];
|
|
|
|
// Returns the mouse location in screen coordinates
|
|
x = (int32_t)mouseLoc.x;
|
|
y = (int32_t)mouseLoc.y;
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
/**
|
|
* \note expect Cocoa screen coordinates.
|
|
*/
|
|
GHOST_TSuccess GHOST_SystemCocoa::setCursorPosition(int32_t x, int32_t y)
|
|
{
|
|
GHOST_WindowCocoa *window = (GHOST_WindowCocoa *)m_windowManager->getActiveWindow();
|
|
if (!window) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
// Cursor and mouse dissociation placed here not to interfere with continuous grab
|
|
// (in cont. grab setMouseCursorPosition is directly called)
|
|
CGAssociateMouseAndMouseCursorPosition(false);
|
|
setMouseCursorPosition(x, y);
|
|
CGAssociateMouseAndMouseCursorPosition(true);
|
|
|
|
// Force mouse move event (not pushed by Cocoa)
|
|
pushEvent(new GHOST_EventCursor(
|
|
getMilliSeconds(), GHOST_kEventCursorMove, window, x, y, window->GetCocoaTabletData()));
|
|
m_outsideLoopEventProcessed = true;
|
|
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemCocoa::getPixelAtCursor(float r_color[3]) const
|
|
{
|
|
/* NOTE: There are known issues/limitations at the moment:
|
|
*
|
|
* - User needs to allow screen capture permission for Blender.
|
|
* - Blender has no control of the cursor outside of its window, so it is
|
|
* not going to be the eyedropper icon.
|
|
* - GHOST does not report click events from outside of the window, so the
|
|
* user needs to press Enter instead.
|
|
*
|
|
* Ref #111303.
|
|
*/
|
|
|
|
@autoreleasepool {
|
|
/* Check for screen capture access permission early to prevent issues.
|
|
* Without permission, macOS may capture only the Blender window, wallpaper, and taskbar.
|
|
* This behavior could confuse users, especially when trying to pick a color from another app,
|
|
* potentially capturing the wallpaper under that app window.
|
|
*/
|
|
if (@available(macOS 11.0, *)) {
|
|
/* Although these methods are documented as available for macOS 10.15, they are not actually
|
|
* shipped, leading to a crash if used on macOS 10.15.
|
|
*
|
|
* Ref: https://developer.apple.com/forums/thread/683860?answerId=684400022#684400022
|
|
*/
|
|
if (!CGPreflightScreenCaptureAccess()) {
|
|
CGRequestScreenCaptureAccess();
|
|
return GHOST_kFailure;
|
|
}
|
|
}
|
|
|
|
CGEventRef event = CGEventCreate(nil);
|
|
if (!event) {
|
|
return GHOST_kFailure;
|
|
}
|
|
CGPoint mouseLocation = CGEventGetLocation(event);
|
|
CFRelease(event);
|
|
|
|
CGRect rect = CGRectMake(mouseLocation.x, mouseLocation.y, 1, 1);
|
|
CGImageRef image = CGWindowListCreateImage(
|
|
rect, kCGWindowListOptionOnScreenOnly, kCGNullWindowID, kCGWindowImageDefault);
|
|
if (!image) {
|
|
return GHOST_kFailure;
|
|
}
|
|
NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithCGImage:image];
|
|
CGImageRelease(image);
|
|
|
|
NSColor *color = [bitmap colorAtX:0 y:0];
|
|
if (!color) {
|
|
return GHOST_kFailure;
|
|
}
|
|
NSColor *srgbColor = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]];
|
|
if (!srgbColor) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
CGFloat red = 0.0, green = 0.0, blue = 0.0;
|
|
[color getRed:&red green:&green blue:&blue alpha:nil];
|
|
r_color[0] = red;
|
|
r_color[1] = green;
|
|
r_color[2] = blue;
|
|
}
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemCocoa::setMouseCursorPosition(int32_t x, int32_t y)
|
|
{
|
|
float xf = (float)x, yf = (float)y;
|
|
GHOST_WindowCocoa *window = (GHOST_WindowCocoa *)m_windowManager->getActiveWindow();
|
|
if (!window) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
@autoreleasepool {
|
|
NSScreen *windowScreen = window->getScreen();
|
|
NSRect screenRect = [windowScreen frame];
|
|
|
|
// Set position relative to current screen
|
|
xf -= screenRect.origin.x;
|
|
yf -= screenRect.origin.y;
|
|
|
|
// Quartz Display Services uses the old coordinates (top left origin)
|
|
yf = screenRect.size.height - yf;
|
|
|
|
CGDisplayMoveCursorToPoint((CGDirectDisplayID)[[[windowScreen deviceDescription]
|
|
objectForKey:@"NSScreenNumber"] unsignedIntValue],
|
|
CGPointMake(xf, yf));
|
|
|
|
// See https://stackoverflow.com/a/17559012. By default, hardware events
|
|
// will be suppressed for 500ms after a synthetic mouse event. For unknown
|
|
// reasons CGEventSourceSetLocalEventsSuppressionInterval does not work,
|
|
// however calling CGAssociateMouseAndMouseCursorPosition also removes the
|
|
// delay, even if this is undocumented.
|
|
CGAssociateMouseAndMouseCursorPosition(true);
|
|
}
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemCocoa::getModifierKeys(GHOST_ModifierKeys &keys) const
|
|
{
|
|
keys.set(GHOST_kModifierKeyLeftOS, (m_modifierMask & NSEventModifierFlagCommand) ? true : false);
|
|
keys.set(GHOST_kModifierKeyLeftAlt, (m_modifierMask & NSEventModifierFlagOption) ? true : false);
|
|
keys.set(GHOST_kModifierKeyLeftShift,
|
|
(m_modifierMask & NSEventModifierFlagShift) ? true : false);
|
|
keys.set(GHOST_kModifierKeyLeftControl,
|
|
(m_modifierMask & NSEventModifierFlagControl) ? true : false);
|
|
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemCocoa::getButtons(GHOST_Buttons &buttons) const
|
|
{
|
|
UInt32 button_state = GetCurrentEventButtonState();
|
|
|
|
buttons.clear();
|
|
buttons.set(GHOST_kButtonMaskLeft, button_state & (1 << 0));
|
|
buttons.set(GHOST_kButtonMaskRight, button_state & (1 << 1));
|
|
buttons.set(GHOST_kButtonMaskMiddle, button_state & (1 << 2));
|
|
buttons.set(GHOST_kButtonMaskButton4, button_state & (1 << 3));
|
|
buttons.set(GHOST_kButtonMaskButton5, button_state & (1 << 4));
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_TCapabilityFlag GHOST_SystemCocoa::getCapabilities() const
|
|
{
|
|
return GHOST_TCapabilityFlag(
|
|
GHOST_CAPABILITY_FLAG_ALL &
|
|
~(
|
|
/* Cocoa has no support for a primary selection clipboard. */
|
|
GHOST_kCapabilityPrimaryClipboard |
|
|
/* This Cocoa back-end has not yet implemented image copy/paste. */
|
|
GHOST_kCapabilityClipboardImages));
|
|
}
|
|
|
|
#pragma mark Event handlers
|
|
|
|
/**
|
|
* The event queue polling function
|
|
*/
|
|
bool GHOST_SystemCocoa::processEvents(bool /*waitForEvent*/)
|
|
{
|
|
bool anyProcessed = false;
|
|
NSEvent *event;
|
|
|
|
/* TODO: implement timer? */
|
|
#if 0
|
|
do {
|
|
GHOST_TimerManager* timerMgr = getTimerManager();
|
|
|
|
if (waitForEvent) {
|
|
uint64_t next = timerMgr->nextFireTime();
|
|
double timeOut;
|
|
|
|
if (next == GHOST_kFireTimeNever) {
|
|
timeOut = kEventDurationForever;
|
|
}
|
|
else {
|
|
timeOut = (double)(next - getMilliSeconds())/1000.0;
|
|
if (timeOut < 0.0)
|
|
timeOut = 0.0;
|
|
}
|
|
|
|
::ReceiveNextEvent(0, nullptr, timeOut, false, &event);
|
|
}
|
|
|
|
if (timerMgr->fireTimers(getMilliSeconds())) {
|
|
anyProcessed = true;
|
|
}
|
|
#endif
|
|
do {
|
|
@autoreleasepool {
|
|
event = [NSApp nextEventMatchingMask:NSEventMaskAny
|
|
untilDate:[NSDate distantPast]
|
|
inMode:NSDefaultRunLoopMode
|
|
dequeue:YES];
|
|
if (event == nil) {
|
|
break;
|
|
}
|
|
|
|
anyProcessed = true;
|
|
|
|
// Send event to NSApp to ensure Mac wide events are handled,
|
|
// this will send events to CocoaWindow which will call back
|
|
// to handleKeyEvent, handleMouseEvent and handleTabletEvent
|
|
|
|
// There is on special exception for ctrl+(shift)+tab. We do not
|
|
// get keyDown events delivered to the view because they are
|
|
// special hotkeys to switch between views, so override directly
|
|
|
|
if ([event type] == NSEventTypeKeyDown && [event keyCode] == kVK_Tab &&
|
|
([event modifierFlags] & NSEventModifierFlagControl))
|
|
{
|
|
handleKeyEvent(event);
|
|
}
|
|
else {
|
|
// For some reason NSApp is swallowing the key up events when modifier
|
|
// key is pressed, even if there seems to be no apparent reason to do
|
|
// so, as a workaround we always handle these up events.
|
|
if ([event type] == NSEventTypeKeyUp &&
|
|
([event modifierFlags] & (NSEventModifierFlagCommand | NSEventModifierFlagOption)))
|
|
{
|
|
handleKeyEvent(event);
|
|
}
|
|
|
|
[NSApp sendEvent:event];
|
|
}
|
|
}
|
|
} while (event != nil);
|
|
#if 0
|
|
} while (waitForEvent && !anyProcessed); // Needed only for timer implementation
|
|
#endif
|
|
|
|
if (m_needDelayedApplicationBecomeActiveEventProcessing) {
|
|
handleApplicationBecomeActiveEvent();
|
|
}
|
|
|
|
if (m_outsideLoopEventProcessed) {
|
|
m_outsideLoopEventProcessed = false;
|
|
return true;
|
|
}
|
|
|
|
m_ignoreWindowSizedMessages = false;
|
|
|
|
return anyProcessed;
|
|
}
|
|
|
|
/* NOTE: called from #NSApplication delegate. */
|
|
GHOST_TSuccess GHOST_SystemCocoa::handleApplicationBecomeActiveEvent()
|
|
{
|
|
for (GHOST_IWindow *iwindow : m_windowManager->getWindows()) {
|
|
GHOST_WindowCocoa *window = (GHOST_WindowCocoa *)iwindow;
|
|
if (window->isDialog()) {
|
|
[window->getCocoaWindow() makeKeyAndOrderFront:nil];
|
|
}
|
|
}
|
|
|
|
// Update the modifiers key mask, as its status may have changed when the application
|
|
// was not active (that is when update events are sent to another application).
|
|
unsigned int modifiers;
|
|
GHOST_IWindow *window = m_windowManager->getActiveWindow();
|
|
|
|
if (!window) {
|
|
m_needDelayedApplicationBecomeActiveEventProcessing = true;
|
|
return GHOST_kFailure;
|
|
}
|
|
else {
|
|
m_needDelayedApplicationBecomeActiveEventProcessing = false;
|
|
}
|
|
|
|
modifiers = [[[NSApplication sharedApplication] currentEvent] modifierFlags];
|
|
|
|
if ((modifiers & NSEventModifierFlagShift) != (m_modifierMask & NSEventModifierFlagShift)) {
|
|
pushEvent(new GHOST_EventKey(getMilliSeconds(),
|
|
(modifiers & NSEventModifierFlagShift) ? GHOST_kEventKeyDown :
|
|
GHOST_kEventKeyUp,
|
|
window,
|
|
GHOST_kKeyLeftShift,
|
|
false));
|
|
}
|
|
if ((modifiers & NSEventModifierFlagControl) != (m_modifierMask & NSEventModifierFlagControl)) {
|
|
pushEvent(new GHOST_EventKey(getMilliSeconds(),
|
|
(modifiers & NSEventModifierFlagControl) ? GHOST_kEventKeyDown :
|
|
GHOST_kEventKeyUp,
|
|
window,
|
|
GHOST_kKeyLeftControl,
|
|
false));
|
|
}
|
|
if ((modifiers & NSEventModifierFlagOption) != (m_modifierMask & NSEventModifierFlagOption)) {
|
|
pushEvent(new GHOST_EventKey(getMilliSeconds(),
|
|
(modifiers & NSEventModifierFlagOption) ? GHOST_kEventKeyDown :
|
|
GHOST_kEventKeyUp,
|
|
window,
|
|
GHOST_kKeyLeftAlt,
|
|
false));
|
|
}
|
|
if ((modifiers & NSEventModifierFlagCommand) != (m_modifierMask & NSEventModifierFlagCommand)) {
|
|
pushEvent(new GHOST_EventKey(getMilliSeconds(),
|
|
(modifiers & NSEventModifierFlagCommand) ? GHOST_kEventKeyDown :
|
|
GHOST_kEventKeyUp,
|
|
window,
|
|
GHOST_kKeyLeftOS,
|
|
false));
|
|
}
|
|
|
|
m_modifierMask = modifiers;
|
|
|
|
m_outsideLoopEventProcessed = true;
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
bool GHOST_SystemCocoa::hasDialogWindow()
|
|
{
|
|
for (GHOST_IWindow *iwindow : m_windowManager->getWindows()) {
|
|
GHOST_WindowCocoa *window = (GHOST_WindowCocoa *)iwindow;
|
|
if (window->isDialog()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void GHOST_SystemCocoa::notifyExternalEventProcessed()
|
|
{
|
|
m_outsideLoopEventProcessed = true;
|
|
}
|
|
|
|
/* NOTE: called from #NSWindow delegate. */
|
|
GHOST_TSuccess GHOST_SystemCocoa::handleWindowEvent(GHOST_TEventType eventType,
|
|
GHOST_WindowCocoa *window)
|
|
{
|
|
if (!validWindow(window)) {
|
|
return GHOST_kFailure;
|
|
}
|
|
switch (eventType) {
|
|
case GHOST_kEventWindowClose:
|
|
pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowClose, window));
|
|
break;
|
|
case GHOST_kEventWindowActivate:
|
|
m_windowManager->setActiveWindow(window);
|
|
window->loadCursor(window->getCursorVisibility(), window->getCursorShape());
|
|
pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowActivate, window));
|
|
break;
|
|
case GHOST_kEventWindowDeactivate:
|
|
m_windowManager->setWindowInactive(window);
|
|
pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowDeactivate, window));
|
|
break;
|
|
case GHOST_kEventWindowUpdate:
|
|
if (m_nativePixel) {
|
|
window->setNativePixelSize();
|
|
pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventNativeResolutionChange, window));
|
|
}
|
|
pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowUpdate, window));
|
|
break;
|
|
case GHOST_kEventWindowMove:
|
|
pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowMove, window));
|
|
break;
|
|
case GHOST_kEventWindowSize:
|
|
if (!m_ignoreWindowSizedMessages) {
|
|
// Enforce only one resize message per event loop
|
|
// (coalescing all the live resize messages)
|
|
window->updateDrawingContext();
|
|
pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventWindowSize, window));
|
|
// Mouse up event is trapped by the resizing event loop,
|
|
// so send it anyway to the window manager.
|
|
pushEvent(new GHOST_EventButton(getMilliSeconds(),
|
|
GHOST_kEventButtonUp,
|
|
window,
|
|
GHOST_kButtonMaskLeft,
|
|
GHOST_TABLET_DATA_NONE));
|
|
// m_ignoreWindowSizedMessages = true;
|
|
}
|
|
break;
|
|
case GHOST_kEventNativeResolutionChange:
|
|
|
|
if (m_nativePixel) {
|
|
window->setNativePixelSize();
|
|
pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventNativeResolutionChange, window));
|
|
}
|
|
|
|
default:
|
|
return GHOST_kFailure;
|
|
break;
|
|
}
|
|
|
|
m_outsideLoopEventProcessed = true;
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
/* NOTE: called from #NSWindow subclass. */
|
|
GHOST_TSuccess GHOST_SystemCocoa::handleDraggingEvent(GHOST_TEventType eventType,
|
|
GHOST_TDragnDropTypes draggedObjectType,
|
|
GHOST_WindowCocoa *window,
|
|
int mouseX,
|
|
int mouseY,
|
|
void *data)
|
|
{
|
|
if (!validWindow(window)) {
|
|
return GHOST_kFailure;
|
|
}
|
|
switch (eventType) {
|
|
case GHOST_kEventDraggingEntered:
|
|
case GHOST_kEventDraggingUpdated:
|
|
case GHOST_kEventDraggingExited:
|
|
window->clientToScreenIntern(mouseX, mouseY, mouseX, mouseY);
|
|
pushEvent(new GHOST_EventDragnDrop(
|
|
getMilliSeconds(), eventType, draggedObjectType, window, mouseX, mouseY, nullptr));
|
|
break;
|
|
|
|
case GHOST_kEventDraggingDropDone: {
|
|
uint8_t *temp_buff;
|
|
GHOST_TStringArray *strArray;
|
|
NSArray *droppedArray;
|
|
size_t pastedTextSize;
|
|
NSString *droppedStr;
|
|
GHOST_TDragnDropDataPtr eventData;
|
|
int i;
|
|
|
|
if (!data) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
switch (draggedObjectType) {
|
|
case GHOST_kDragnDropTypeFilenames:
|
|
droppedArray = (NSArray *)data;
|
|
|
|
strArray = (GHOST_TStringArray *)malloc(sizeof(GHOST_TStringArray));
|
|
if (!strArray) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
strArray->count = [droppedArray count];
|
|
if (strArray->count == 0) {
|
|
free(strArray);
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
strArray->strings = (uint8_t **)malloc(strArray->count * sizeof(uint8_t *));
|
|
|
|
for (i = 0; i < strArray->count; i++) {
|
|
droppedStr = [droppedArray objectAtIndex:i];
|
|
|
|
pastedTextSize = [droppedStr lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
|
|
temp_buff = (uint8_t *)malloc(pastedTextSize + 1);
|
|
|
|
if (!temp_buff) {
|
|
strArray->count = i;
|
|
break;
|
|
}
|
|
|
|
memcpy(
|
|
temp_buff, [droppedStr cStringUsingEncoding:NSUTF8StringEncoding], pastedTextSize);
|
|
temp_buff[pastedTextSize] = '\0';
|
|
|
|
strArray->strings[i] = temp_buff;
|
|
}
|
|
|
|
eventData = (GHOST_TDragnDropDataPtr)strArray;
|
|
break;
|
|
|
|
case GHOST_kDragnDropTypeString:
|
|
droppedStr = (NSString *)data;
|
|
pastedTextSize = [droppedStr lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
|
|
|
|
temp_buff = (uint8_t *)malloc(pastedTextSize + 1);
|
|
|
|
if (temp_buff == nullptr) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
memcpy(
|
|
temp_buff, [droppedStr cStringUsingEncoding:NSUTF8StringEncoding], pastedTextSize);
|
|
temp_buff[pastedTextSize] = '\0';
|
|
|
|
eventData = (GHOST_TDragnDropDataPtr)temp_buff;
|
|
break;
|
|
|
|
case GHOST_kDragnDropTypeBitmap: {
|
|
NSImage *droppedImg = (NSImage *)data;
|
|
NSSize imgSize = [droppedImg size];
|
|
ImBuf *ibuf = nullptr;
|
|
uint8_t *rasterRGB = nullptr;
|
|
uint8_t *rasterRGBA = nullptr;
|
|
uint8_t *toIBuf = nullptr;
|
|
int x, y, to_i, from_i;
|
|
NSBitmapImageRep *blBitmapFormatImageRGB, *blBitmapFormatImageRGBA, *bitmapImage = nil;
|
|
NSEnumerator *enumerator;
|
|
NSImageRep *representation;
|
|
|
|
ibuf = IMB_allocImBuf(imgSize.width, imgSize.height, 32, IB_rect);
|
|
if (!ibuf) {
|
|
[droppedImg release];
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
/* Get the bitmap of the image. */
|
|
enumerator = [[droppedImg representations] objectEnumerator];
|
|
while ((representation = [enumerator nextObject])) {
|
|
if ([representation isKindOfClass:[NSBitmapImageRep class]]) {
|
|
bitmapImage = (NSBitmapImageRep *)representation;
|
|
break;
|
|
}
|
|
}
|
|
if (bitmapImage == nil) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
if (([bitmapImage bitsPerPixel] == 32) && (([bitmapImage bitmapFormat] & 0x5) == 0) &&
|
|
![bitmapImage isPlanar])
|
|
{
|
|
/* Try a fast copy if the image is a meshed RGBA 32bit bitmap. */
|
|
toIBuf = ibuf->byte_buffer.data;
|
|
rasterRGB = (uint8_t *)[bitmapImage bitmapData];
|
|
for (y = 0; y < imgSize.height; y++) {
|
|
to_i = (imgSize.height - y - 1) * imgSize.width;
|
|
from_i = y * imgSize.width;
|
|
memcpy(toIBuf + 4 * to_i, rasterRGB + 4 * from_i, 4 * imgSize.width);
|
|
}
|
|
}
|
|
else {
|
|
/* Tell cocoa image resolution is same as current system one */
|
|
[bitmapImage setSize:imgSize];
|
|
|
|
/* Convert the image in a RGBA 32bit format */
|
|
/* As Core Graphics does not support contexts with non premutliplied alpha,
|
|
* we need to get alpha key values in a separate batch */
|
|
|
|
/* First get RGB values w/o Alpha to avoid pre-multiplication,
|
|
* 32bit but last byte is unused */
|
|
blBitmapFormatImageRGB = [[NSBitmapImageRep alloc]
|
|
initWithBitmapDataPlanes:nullptr
|
|
pixelsWide:imgSize.width
|
|
pixelsHigh:imgSize.height
|
|
bitsPerSample:8
|
|
samplesPerPixel:3
|
|
hasAlpha:NO
|
|
isPlanar:NO
|
|
colorSpaceName:NSDeviceRGBColorSpace
|
|
bitmapFormat:(NSBitmapFormat)0
|
|
bytesPerRow:4 * imgSize.width
|
|
bitsPerPixel:32 /* RGB format padded to 32bits. */];
|
|
|
|
[NSGraphicsContext saveGraphicsState];
|
|
[NSGraphicsContext
|
|
setCurrentContext:[NSGraphicsContext
|
|
graphicsContextWithBitmapImageRep:blBitmapFormatImageRGB]];
|
|
[bitmapImage draw];
|
|
[NSGraphicsContext restoreGraphicsState];
|
|
|
|
rasterRGB = (uint8_t *)[blBitmapFormatImageRGB bitmapData];
|
|
if (rasterRGB == nullptr) {
|
|
[bitmapImage release];
|
|
[blBitmapFormatImageRGB release];
|
|
[droppedImg release];
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
/* Then get Alpha values by getting the RGBA image (that is pre-multiplied BTW) */
|
|
blBitmapFormatImageRGBA = [[NSBitmapImageRep alloc]
|
|
initWithBitmapDataPlanes:nullptr
|
|
pixelsWide:imgSize.width
|
|
pixelsHigh:imgSize.height
|
|
bitsPerSample:8
|
|
samplesPerPixel:4
|
|
hasAlpha:YES
|
|
isPlanar:NO
|
|
colorSpaceName:NSDeviceRGBColorSpace
|
|
bitmapFormat:(NSBitmapFormat)0
|
|
bytesPerRow:4 * imgSize.width
|
|
bitsPerPixel:32 /* RGBA */];
|
|
|
|
[NSGraphicsContext saveGraphicsState];
|
|
[NSGraphicsContext
|
|
setCurrentContext:[NSGraphicsContext
|
|
graphicsContextWithBitmapImageRep:blBitmapFormatImageRGBA]];
|
|
[bitmapImage draw];
|
|
[NSGraphicsContext restoreGraphicsState];
|
|
|
|
rasterRGBA = (uint8_t *)[blBitmapFormatImageRGBA bitmapData];
|
|
if (rasterRGBA == nullptr) {
|
|
[bitmapImage release];
|
|
[blBitmapFormatImageRGB release];
|
|
[blBitmapFormatImageRGBA release];
|
|
[droppedImg release];
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
/* Copy the image to ibuf, flipping it vertically. */
|
|
toIBuf = ibuf->byte_buffer.data;
|
|
for (y = 0; y < imgSize.height; y++) {
|
|
for (x = 0; x < imgSize.width; x++) {
|
|
to_i = (imgSize.height - y - 1) * imgSize.width + x;
|
|
from_i = y * imgSize.width + x;
|
|
|
|
toIBuf[4 * to_i] = rasterRGB[4 * from_i]; /* R */
|
|
toIBuf[4 * to_i + 1] = rasterRGB[4 * from_i + 1]; /* G */
|
|
toIBuf[4 * to_i + 2] = rasterRGB[4 * from_i + 2]; /* B */
|
|
toIBuf[4 * to_i + 3] = rasterRGBA[4 * from_i + 3]; /* A */
|
|
}
|
|
}
|
|
|
|
[blBitmapFormatImageRGB release];
|
|
[blBitmapFormatImageRGBA release];
|
|
[droppedImg release];
|
|
}
|
|
|
|
eventData = (GHOST_TDragnDropDataPtr)ibuf;
|
|
|
|
break;
|
|
}
|
|
default:
|
|
return GHOST_kFailure;
|
|
break;
|
|
}
|
|
|
|
window->clientToScreenIntern(mouseX, mouseY, mouseX, mouseY);
|
|
pushEvent(new GHOST_EventDragnDrop(
|
|
getMilliSeconds(), eventType, draggedObjectType, window, mouseX, mouseY, eventData));
|
|
|
|
break;
|
|
}
|
|
default:
|
|
return GHOST_kFailure;
|
|
}
|
|
m_outsideLoopEventProcessed = true;
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
void GHOST_SystemCocoa::handleQuitRequest()
|
|
{
|
|
GHOST_Window *window = (GHOST_Window *)m_windowManager->getActiveWindow();
|
|
|
|
// Discard quit event if we are in cursor grab sequence
|
|
if (window && window->getCursorGrabModeIsWarp()) {
|
|
return;
|
|
}
|
|
|
|
// Push the event to Blender so it can open a dialog if needed
|
|
pushEvent(new GHOST_Event(getMilliSeconds(), GHOST_kEventQuitRequest, window));
|
|
m_outsideLoopEventProcessed = true;
|
|
}
|
|
|
|
bool GHOST_SystemCocoa::handleOpenDocumentRequest(void *filepathStr)
|
|
{
|
|
NSString *filepath = (NSString *)filepathStr;
|
|
NSArray *windowsList;
|
|
char *temp_buff;
|
|
size_t filenameTextSize;
|
|
|
|
/* Check for blender opened windows and make the frontmost key. In case blender
|
|
* is minimized, opened on another desktop space, or in full-screen mode. */
|
|
windowsList = [NSApp orderedWindows];
|
|
if ([windowsList count]) {
|
|
[[windowsList objectAtIndex:0] makeKeyAndOrderFront:nil];
|
|
}
|
|
|
|
GHOST_Window *window = m_windowManager->getWindows().empty() ?
|
|
nullptr :
|
|
(GHOST_Window *)m_windowManager->getWindows().front();
|
|
|
|
if (!window) {
|
|
return NO;
|
|
}
|
|
|
|
/* Discard event if we are in cursor grab sequence,
|
|
* it'll lead to "stuck cursor" situation if the alert panel is raised. */
|
|
if (window && window->getCursorGrabModeIsWarp()) {
|
|
return NO;
|
|
}
|
|
|
|
filenameTextSize = [filepath lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
|
|
temp_buff = (char *)malloc(filenameTextSize + 1);
|
|
|
|
if (temp_buff == nullptr) {
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
memcpy(temp_buff, [filepath cStringUsingEncoding:NSUTF8StringEncoding], filenameTextSize);
|
|
temp_buff[filenameTextSize] = '\0';
|
|
|
|
pushEvent(new GHOST_EventString(
|
|
getMilliSeconds(), GHOST_kEventOpenMainFile, window, (GHOST_TEventDataPtr)temp_buff));
|
|
|
|
return YES;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemCocoa::handleTabletEvent(void *eventPtr, short eventType)
|
|
{
|
|
NSEvent *event = (NSEvent *)eventPtr;
|
|
GHOST_IWindow *window;
|
|
|
|
window = m_windowManager->getWindowAssociatedWithOSWindow((const void *)[event window]);
|
|
if (!window) {
|
|
// printf("\nW failure for event 0x%x",[event type]);
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
GHOST_TabletData &ct = ((GHOST_WindowCocoa *)window)->GetCocoaTabletData();
|
|
|
|
switch (eventType) {
|
|
case NSEventTypeTabletPoint:
|
|
// workaround 2 cornercases:
|
|
// 1. if [event isEnteringProximity] was not triggered since program-start
|
|
// 2. device is not sending [event pointingDeviceType], due no eraser
|
|
if (ct.Active == GHOST_kTabletModeNone) {
|
|
ct.Active = GHOST_kTabletModeStylus;
|
|
}
|
|
|
|
ct.Pressure = [event pressure];
|
|
ct.Xtilt = [event tilt].x;
|
|
ct.Ytilt = [event tilt].y;
|
|
break;
|
|
|
|
case NSEventTypeTabletProximity:
|
|
/* Reset tablet data when device enters proximity or leaves. */
|
|
ct = GHOST_TABLET_DATA_NONE;
|
|
if ([event isEnteringProximity]) {
|
|
/* Pointer is entering tablet area proximity. */
|
|
switch ([event pointingDeviceType]) {
|
|
case NSPointingDeviceTypePen:
|
|
ct.Active = GHOST_kTabletModeStylus;
|
|
break;
|
|
case NSPointingDeviceTypeEraser:
|
|
ct.Active = GHOST_kTabletModeEraser;
|
|
break;
|
|
case NSPointingDeviceTypeCursor:
|
|
case NSPointingDeviceTypeUnknown:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
GHOST_ASSERT(FALSE, "GHOST_SystemCocoa::handleTabletEvent : unknown event received");
|
|
return GHOST_kFailure;
|
|
break;
|
|
}
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
bool GHOST_SystemCocoa::handleTabletEvent(void *eventPtr)
|
|
{
|
|
NSEvent *event = (NSEvent *)eventPtr;
|
|
|
|
switch ([event subtype]) {
|
|
case NSEventSubtypeTabletPoint:
|
|
handleTabletEvent(eventPtr, NSEventTypeTabletPoint);
|
|
return true;
|
|
case NSEventSubtypeTabletProximity:
|
|
handleTabletEvent(eventPtr, NSEventTypeTabletProximity);
|
|
return true;
|
|
default:
|
|
// No tablet event included : do nothing
|
|
return false;
|
|
}
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemCocoa::handleMouseEvent(void *eventPtr)
|
|
{
|
|
NSEvent *event = (NSEvent *)eventPtr;
|
|
GHOST_WindowCocoa *window;
|
|
CocoaWindow *cocoawindow;
|
|
|
|
/* [event window] returns other windows if mouse-over, that's OSX input standard
|
|
* however, if mouse exits window(s), the windows become inactive, until you click.
|
|
* We then fall back to the active window from ghost. */
|
|
window = (GHOST_WindowCocoa *)m_windowManager->getWindowAssociatedWithOSWindow(
|
|
(const void *)[event window]);
|
|
if (!window) {
|
|
window = (GHOST_WindowCocoa *)m_windowManager->getActiveWindow();
|
|
if (!window) {
|
|
// printf("\nW failure for event 0x%x",[event type]);
|
|
return GHOST_kFailure;
|
|
}
|
|
}
|
|
|
|
cocoawindow = (CocoaWindow *)window->getOSWindow();
|
|
|
|
switch ([event type]) {
|
|
case NSEventTypeLeftMouseDown:
|
|
handleTabletEvent(event); // Update window tablet state to be included in event.
|
|
pushEvent(new GHOST_EventButton([event timestamp] * 1000,
|
|
GHOST_kEventButtonDown,
|
|
window,
|
|
GHOST_kButtonMaskLeft,
|
|
window -> GetCocoaTabletData()));
|
|
break;
|
|
case NSEventTypeRightMouseDown:
|
|
handleTabletEvent(event); // Update window tablet state to be included in event.
|
|
pushEvent(new GHOST_EventButton([event timestamp] * 1000,
|
|
GHOST_kEventButtonDown,
|
|
window,
|
|
GHOST_kButtonMaskRight,
|
|
window -> GetCocoaTabletData()));
|
|
break;
|
|
case NSEventTypeOtherMouseDown:
|
|
handleTabletEvent(event); // Handle tablet events combined with mouse events
|
|
pushEvent(new GHOST_EventButton([event timestamp] * 1000,
|
|
GHOST_kEventButtonDown,
|
|
window,
|
|
convertButton([event buttonNumber]),
|
|
window -> GetCocoaTabletData()));
|
|
break;
|
|
|
|
case NSEventTypeLeftMouseUp:
|
|
handleTabletEvent(event); // Update window tablet state to be included in event.
|
|
pushEvent(new GHOST_EventButton([event timestamp] * 1000,
|
|
GHOST_kEventButtonUp,
|
|
window,
|
|
GHOST_kButtonMaskLeft,
|
|
window -> GetCocoaTabletData()));
|
|
break;
|
|
case NSEventTypeRightMouseUp:
|
|
handleTabletEvent(event); // Update window tablet state to be included in event.
|
|
pushEvent(new GHOST_EventButton([event timestamp] * 1000,
|
|
GHOST_kEventButtonUp,
|
|
window,
|
|
GHOST_kButtonMaskRight,
|
|
window -> GetCocoaTabletData()));
|
|
break;
|
|
case NSEventTypeOtherMouseUp:
|
|
handleTabletEvent(event); // Update window tablet state to be included in event.
|
|
pushEvent(new GHOST_EventButton([event timestamp] * 1000,
|
|
GHOST_kEventButtonUp,
|
|
window,
|
|
convertButton([event buttonNumber]),
|
|
window -> GetCocoaTabletData()));
|
|
break;
|
|
|
|
case NSEventTypeLeftMouseDragged:
|
|
case NSEventTypeRightMouseDragged:
|
|
case NSEventTypeOtherMouseDragged:
|
|
handleTabletEvent(event); // Update window tablet state to be included in event.
|
|
|
|
case NSEventTypeMouseMoved: {
|
|
GHOST_TGrabCursorMode grab_mode = window->getCursorGrabMode();
|
|
|
|
/* TODO: CHECK IF THIS IS A TABLET EVENT */
|
|
bool is_tablet = false;
|
|
|
|
if (is_tablet && window->getCursorGrabModeIsWarp()) {
|
|
grab_mode = GHOST_kGrabDisable;
|
|
}
|
|
|
|
switch (grab_mode) {
|
|
case GHOST_kGrabHide: // Cursor hidden grab operation : no cursor move
|
|
{
|
|
int32_t x_warp, y_warp, x_accum, y_accum, x, y;
|
|
|
|
window->getCursorGrabInitPos(x_warp, y_warp);
|
|
window->screenToClientIntern(x_warp, y_warp, x_warp, y_warp);
|
|
|
|
window->getCursorGrabAccum(x_accum, y_accum);
|
|
x_accum += [event deltaX];
|
|
y_accum += -[event
|
|
deltaY]; // Strange Apple implementation (inverted coordinates for the deltaY) ...
|
|
window->setCursorGrabAccum(x_accum, y_accum);
|
|
|
|
window->clientToScreenIntern(x_warp + x_accum, y_warp + y_accum, x, y);
|
|
pushEvent(new GHOST_EventCursor([event timestamp] * 1000,
|
|
GHOST_kEventCursorMove,
|
|
window,
|
|
x,
|
|
y,
|
|
window -> GetCocoaTabletData()));
|
|
break;
|
|
}
|
|
case GHOST_kGrabWrap: // Wrap cursor at area/window boundaries
|
|
{
|
|
NSTimeInterval timestamp = [event timestamp];
|
|
if (timestamp < m_last_warp_timestamp) {
|
|
/* After warping we can still receive older unwrapped mouse events,
|
|
* ignore those. */
|
|
break;
|
|
}
|
|
|
|
NSPoint mousePos = [event locationInWindow];
|
|
int32_t x_mouse = mousePos.x;
|
|
int32_t y_mouse = mousePos.y;
|
|
GHOST_Rect bounds, windowBounds, correctedBounds;
|
|
|
|
/* fallback to window bounds */
|
|
if (window->getCursorGrabBounds(bounds) == GHOST_kFailure) {
|
|
window->getClientBounds(bounds);
|
|
}
|
|
|
|
/* Switch back to Cocoa coordinates orientation
|
|
* (y=0 at bottom, the same as blender internal BTW!), and to client coordinates. */
|
|
window->getClientBounds(windowBounds);
|
|
window->screenToClient(bounds.m_l, bounds.m_b, correctedBounds.m_l, correctedBounds.m_t);
|
|
window->screenToClient(bounds.m_r, bounds.m_t, correctedBounds.m_r, correctedBounds.m_b);
|
|
correctedBounds.m_b = (windowBounds.m_b - windowBounds.m_t) - correctedBounds.m_b;
|
|
correctedBounds.m_t = (windowBounds.m_b - windowBounds.m_t) - correctedBounds.m_t;
|
|
|
|
// Get accumulation from previous mouse warps
|
|
int32_t x_accum, y_accum;
|
|
window->getCursorGrabAccum(x_accum, y_accum);
|
|
|
|
// Warp mouse cursor if needed
|
|
int32_t warped_x_mouse = x_mouse;
|
|
int32_t warped_y_mouse = y_mouse;
|
|
|
|
correctedBounds.wrapPoint(
|
|
warped_x_mouse, warped_y_mouse, 4, window->getCursorGrabAxis());
|
|
|
|
// Set new cursor position
|
|
if (x_mouse != warped_x_mouse || y_mouse != warped_y_mouse) {
|
|
int32_t warped_x, warped_y;
|
|
window->clientToScreenIntern(warped_x_mouse, warped_y_mouse, warped_x, warped_y);
|
|
setMouseCursorPosition(warped_x, warped_y); /* wrap */
|
|
window->setCursorGrabAccum(x_accum + (x_mouse - warped_x_mouse),
|
|
y_accum + (y_mouse - warped_y_mouse));
|
|
|
|
/* This is the current time that matches NSEvent timestamp. */
|
|
m_last_warp_timestamp = [[NSProcessInfo processInfo] systemUptime];
|
|
}
|
|
|
|
// Generate event
|
|
int32_t x, y;
|
|
window->clientToScreenIntern(x_mouse + x_accum, y_mouse + y_accum, x, y);
|
|
pushEvent(new GHOST_EventCursor([event timestamp] * 1000,
|
|
GHOST_kEventCursorMove,
|
|
window,
|
|
x,
|
|
y,
|
|
window -> GetCocoaTabletData()));
|
|
break;
|
|
}
|
|
default: {
|
|
// Normal cursor operation: send mouse position in window
|
|
NSPoint mousePos = [event locationInWindow];
|
|
int32_t x, y;
|
|
|
|
window->clientToScreenIntern(mousePos.x, mousePos.y, x, y);
|
|
pushEvent(new GHOST_EventCursor([event timestamp] * 1000,
|
|
GHOST_kEventCursorMove,
|
|
window,
|
|
x,
|
|
y,
|
|
window -> GetCocoaTabletData()));
|
|
break;
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case NSEventTypeScrollWheel: {
|
|
NSEventPhase momentumPhase = NSEventPhaseNone;
|
|
NSEventPhase phase = NSEventPhaseNone;
|
|
|
|
momentumPhase = [event momentumPhase];
|
|
phase = [event phase];
|
|
|
|
/* when pressing a key while momentum scrolling continues after
|
|
* lifting fingers off the trackpad, the action can unexpectedly
|
|
* change from e.g. scrolling to zooming. this works around the
|
|
* issue by ignoring momentum scroll after a key press */
|
|
if (momentumPhase) {
|
|
if (m_ignoreMomentumScroll) {
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
m_ignoreMomentumScroll = false;
|
|
}
|
|
|
|
/* we assume phases are only set for gestures from trackpad or magic
|
|
* mouse events. note that using tablet at the same time may not work
|
|
* since this is a static variable */
|
|
if (phase == NSEventPhaseBegan && m_multitouchGestures) {
|
|
m_multiTouchScroll = true;
|
|
}
|
|
else if (phase == NSEventPhaseEnded) {
|
|
m_multiTouchScroll = false;
|
|
}
|
|
|
|
/* Standard scroll-wheel case, if no swiping happened,
|
|
* and no momentum (kinetic scroll) works. */
|
|
if (!m_multiTouchScroll && momentumPhase == NSEventPhaseNone) {
|
|
int32_t delta;
|
|
|
|
double deltaF = [event deltaY];
|
|
|
|
if (deltaF == 0.0) {
|
|
deltaF = [event deltaX]; // make blender decide if it's horizontal scroll
|
|
}
|
|
if (deltaF == 0.0) {
|
|
break; // discard trackpad delta=0 events
|
|
}
|
|
|
|
delta = deltaF > 0.0 ? 1 : -1;
|
|
pushEvent(new GHOST_EventWheel([event timestamp] * 1000, window, delta));
|
|
}
|
|
else {
|
|
NSPoint mousePos = [event locationInWindow];
|
|
int32_t x, y;
|
|
double dx;
|
|
double dy;
|
|
|
|
/* with 10.7 nice scrolling deltas are supported */
|
|
dx = [event scrollingDeltaX];
|
|
dy = [event scrollingDeltaY];
|
|
|
|
/* However, WACOM tablet (intuos5) needs old deltas,
|
|
* it then has momentum and phase at zero. */
|
|
if (phase == NSEventPhaseNone && momentumPhase == NSEventPhaseNone) {
|
|
dx = [event deltaX];
|
|
dy = [event deltaY];
|
|
}
|
|
window->clientToScreenIntern(mousePos.x, mousePos.y, x, y);
|
|
|
|
NSPoint delta = [[cocoawindow contentView] convertPointToBacking:NSMakePoint(dx, dy)];
|
|
pushEvent(new GHOST_EventTrackpad([event timestamp] * 1000,
|
|
window,
|
|
GHOST_kTrackpadEventScroll,
|
|
x,
|
|
y,
|
|
delta.x,
|
|
delta.y,
|
|
[event isDirectionInvertedFromDevice]));
|
|
}
|
|
} break;
|
|
|
|
case NSEventTypeMagnify: {
|
|
NSPoint mousePos = [event locationInWindow];
|
|
int32_t x, y;
|
|
window->clientToScreenIntern(mousePos.x, mousePos.y, x, y);
|
|
pushEvent(new GHOST_EventTrackpad([event timestamp] * 1000,
|
|
window,
|
|
GHOST_kTrackpadEventMagnify,
|
|
x,
|
|
y,
|
|
[event magnification] * 125.0 + 0.1,
|
|
0,
|
|
false));
|
|
} break;
|
|
|
|
case NSEventTypeSmartMagnify: {
|
|
NSPoint mousePos = [event locationInWindow];
|
|
int32_t x, y;
|
|
window->clientToScreenIntern(mousePos.x, mousePos.y, x, y);
|
|
pushEvent(new GHOST_EventTrackpad(
|
|
[event timestamp] * 1000, window, GHOST_kTrackpadEventSmartMagnify, x, y, 0, 0, false));
|
|
} break;
|
|
|
|
case NSEventTypeRotate: {
|
|
NSPoint mousePos = [event locationInWindow];
|
|
int32_t x, y;
|
|
window->clientToScreenIntern(mousePos.x, mousePos.y, x, y);
|
|
pushEvent(new GHOST_EventTrackpad([event timestamp] * 1000,
|
|
window,
|
|
GHOST_kTrackpadEventRotate,
|
|
x,
|
|
y,
|
|
[event rotation] * -5.0,
|
|
0,
|
|
false));
|
|
}
|
|
default:
|
|
return GHOST_kFailure;
|
|
break;
|
|
}
|
|
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemCocoa::handleKeyEvent(void *eventPtr)
|
|
{
|
|
NSEvent *event = (NSEvent *)eventPtr;
|
|
GHOST_IWindow *window;
|
|
unsigned int modifiers;
|
|
NSString *characters;
|
|
NSData *convertedCharacters;
|
|
GHOST_TKey keyCode;
|
|
NSString *charsIgnoringModifiers;
|
|
|
|
window = m_windowManager->getWindowAssociatedWithOSWindow((const void *)[event window]);
|
|
if (!window) {
|
|
// printf("\nW failure for event 0x%x",[event type]);
|
|
return GHOST_kFailure;
|
|
}
|
|
|
|
char utf8_buf[6] = {'\0'};
|
|
|
|
switch ([event type]) {
|
|
|
|
case NSEventTypeKeyDown:
|
|
case NSEventTypeKeyUp:
|
|
/* Returns an empty string for dead keys. */
|
|
charsIgnoringModifiers = [event charactersIgnoringModifiers];
|
|
if ([charsIgnoringModifiers length] > 0) {
|
|
keyCode = convertKey([event keyCode], [charsIgnoringModifiers characterAtIndex:0]);
|
|
}
|
|
else {
|
|
keyCode = convertKey([event keyCode], 0);
|
|
}
|
|
|
|
characters = [event characters];
|
|
if ([characters length] > 0) {
|
|
convertedCharacters = [characters dataUsingEncoding:NSUTF8StringEncoding];
|
|
|
|
for (int x = 0; x < [convertedCharacters length]; x++) {
|
|
utf8_buf[x] = ((char *)[convertedCharacters bytes])[x];
|
|
}
|
|
}
|
|
|
|
/* arrow keys should not have utf8 */
|
|
if ((keyCode >= GHOST_kKeyLeftArrow) && (keyCode <= GHOST_kKeyDownArrow)) {
|
|
utf8_buf[0] = '\0';
|
|
}
|
|
|
|
/* F keys should not have utf8 */
|
|
if ((keyCode >= GHOST_kKeyF1) && (keyCode <= GHOST_kKeyF20)) {
|
|
utf8_buf[0] = '\0';
|
|
}
|
|
|
|
/* no text with command key pressed */
|
|
if (m_modifierMask & NSEventModifierFlagCommand) {
|
|
utf8_buf[0] = '\0';
|
|
}
|
|
|
|
if ((keyCode == GHOST_kKeyQ) && (m_modifierMask & NSEventModifierFlagCommand)) {
|
|
break; // Cmd-Q is directly handled by Cocoa
|
|
}
|
|
|
|
if ([event type] == NSEventTypeKeyDown) {
|
|
pushEvent(new GHOST_EventKey([event timestamp] * 1000,
|
|
GHOST_kEventKeyDown,
|
|
window,
|
|
keyCode,
|
|
[event isARepeat],
|
|
utf8_buf));
|
|
#if 0
|
|
printf("Key down rawCode=0x%x charsIgnoringModifiers=%c keyCode=%u utf8=%s\n",
|
|
[event keyCode],
|
|
[charsIgnoringModifiers length] > 0 ? [charsIgnoringModifiers characterAtIndex:0] :
|
|
' ',
|
|
keyCode,
|
|
utf8_buf);
|
|
#endif
|
|
}
|
|
else {
|
|
pushEvent(new GHOST_EventKey(
|
|
[event timestamp] * 1000, GHOST_kEventKeyUp, window, keyCode, false, nullptr));
|
|
#if 0
|
|
printf("Key up rawCode=0x%x charsIgnoringModifiers=%c keyCode=%u utf8=%s\n",
|
|
[event keyCode],
|
|
[charsIgnoringModifiers length] > 0 ? [charsIgnoringModifiers characterAtIndex:0] :
|
|
' ',
|
|
keyCode,
|
|
utf8_buf);
|
|
#endif
|
|
}
|
|
m_ignoreMomentumScroll = true;
|
|
break;
|
|
|
|
case NSEventTypeFlagsChanged:
|
|
modifiers = [event modifierFlags];
|
|
|
|
if ((modifiers & NSEventModifierFlagShift) != (m_modifierMask & NSEventModifierFlagShift)) {
|
|
pushEvent(new GHOST_EventKey([event timestamp] * 1000,
|
|
(modifiers & NSEventModifierFlagShift) ? GHOST_kEventKeyDown :
|
|
GHOST_kEventKeyUp,
|
|
window,
|
|
GHOST_kKeyLeftShift,
|
|
false));
|
|
}
|
|
if ((modifiers & NSEventModifierFlagControl) !=
|
|
(m_modifierMask & NSEventModifierFlagControl)) {
|
|
pushEvent(new GHOST_EventKey(
|
|
[event timestamp] * 1000,
|
|
(modifiers & NSEventModifierFlagControl) ? GHOST_kEventKeyDown : GHOST_kEventKeyUp,
|
|
window,
|
|
GHOST_kKeyLeftControl,
|
|
false));
|
|
}
|
|
if ((modifiers & NSEventModifierFlagOption) != (m_modifierMask & NSEventModifierFlagOption))
|
|
{
|
|
pushEvent(new GHOST_EventKey(
|
|
[event timestamp] * 1000,
|
|
(modifiers & NSEventModifierFlagOption) ? GHOST_kEventKeyDown : GHOST_kEventKeyUp,
|
|
window,
|
|
GHOST_kKeyLeftAlt,
|
|
false));
|
|
}
|
|
if ((modifiers & NSEventModifierFlagCommand) !=
|
|
(m_modifierMask & NSEventModifierFlagCommand)) {
|
|
pushEvent(new GHOST_EventKey(
|
|
[event timestamp] * 1000,
|
|
(modifiers & NSEventModifierFlagCommand) ? GHOST_kEventKeyDown : GHOST_kEventKeyUp,
|
|
window,
|
|
GHOST_kKeyLeftOS,
|
|
false));
|
|
}
|
|
|
|
m_modifierMask = modifiers;
|
|
m_ignoreMomentumScroll = true;
|
|
break;
|
|
|
|
default:
|
|
return GHOST_kFailure;
|
|
break;
|
|
}
|
|
|
|
return GHOST_kSuccess;
|
|
}
|
|
|
|
#pragma mark Clipboard get/set
|
|
|
|
char *GHOST_SystemCocoa::getClipboard(bool /*selection*/) const
|
|
{
|
|
char *temp_buff;
|
|
size_t pastedTextSize;
|
|
|
|
@autoreleasepool {
|
|
|
|
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
|
|
|
|
NSString *textPasted = [pasteBoard stringForType:NSPasteboardTypeString];
|
|
|
|
if (textPasted == nil) {
|
|
return nullptr;
|
|
}
|
|
|
|
pastedTextSize = [textPasted lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
|
|
|
|
temp_buff = (char *)malloc(pastedTextSize + 1);
|
|
|
|
if (temp_buff == nullptr) {
|
|
return nullptr;
|
|
}
|
|
|
|
memcpy(temp_buff, [textPasted cStringUsingEncoding:NSUTF8StringEncoding], pastedTextSize);
|
|
temp_buff[pastedTextSize] = '\0';
|
|
|
|
if (temp_buff) {
|
|
return temp_buff;
|
|
}
|
|
else {
|
|
return nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
void GHOST_SystemCocoa::putClipboard(const char *buffer, bool selection) const
|
|
{
|
|
if (selection) {
|
|
return; // for copying the selection, used on X11
|
|
}
|
|
|
|
@autoreleasepool {
|
|
|
|
NSPasteboard *pasteBoard = NSPasteboard.generalPasteboard;
|
|
[pasteBoard declareTypes:@[ NSPasteboardTypeString ] owner:nil];
|
|
NSString *textToCopy = [NSString stringWithCString:buffer encoding:NSUTF8StringEncoding];
|
|
[pasteBoard setString:textToCopy forType:NSPasteboardTypeString];
|
|
}
|
|
}
|
|
|
|
GHOST_TSuccess GHOST_SystemCocoa::showMessageBox(const char *title,
|
|
const char *message,
|
|
const char *help_label,
|
|
const char *continue_label,
|
|
const char *link,
|
|
GHOST_DialogOptions dialog_options) const
|
|
{
|
|
@autoreleasepool {
|
|
NSAlert *alert = [[[NSAlert alloc] init] autorelease];
|
|
[alert setAccessoryView:[[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 500, 0)] autorelease]];
|
|
|
|
NSString *titleString = [NSString stringWithCString:title];
|
|
NSString *messageString = [NSString stringWithCString:message];
|
|
NSString *continueString = [NSString stringWithCString:continue_label];
|
|
NSString *helpString = [NSString stringWithCString:help_label];
|
|
|
|
if (dialog_options & GHOST_DialogError) {
|
|
[alert setAlertStyle:NSAlertStyleCritical];
|
|
}
|
|
else if (dialog_options & GHOST_DialogWarning) {
|
|
[alert setAlertStyle:NSAlertStyleWarning];
|
|
}
|
|
else {
|
|
[alert setAlertStyle:NSAlertStyleInformational];
|
|
}
|
|
|
|
[alert setMessageText:titleString];
|
|
[alert setInformativeText:messageString];
|
|
|
|
[alert addButtonWithTitle:continueString];
|
|
if (link && strlen(link)) {
|
|
[alert addButtonWithTitle:helpString];
|
|
}
|
|
|
|
NSModalResponse response = [alert runModal];
|
|
if (response == NSAlertSecondButtonReturn) {
|
|
NSString *linkString = [NSString stringWithCString:link];
|
|
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:linkString]];
|
|
}
|
|
}
|
|
return GHOST_kSuccess;
|
|
}
|