/* * tkMacOSXHLEvents.c -- * * Implements high level event support for the Macintosh. * * Copyright © 1995-1997 Sun Microsystems, Inc. * Copyright © 2001-2009 Apple Inc. * Copyright © 2006-2009 Daniel A. Steffen * Copyright © 2015-2019 Marc Culler * Copyright © 2019 Kevin Walzer/WordTech Communications LLC. * * See the file "license.terms" for information on usage and redistribution * of this file, and for a DISCLAIMER OF ALL WARRANTIES. */ #include "tkMacOSXPrivate.h" #include #define URL_MAX_LENGTH (17 + MAXPATHLEN) /* * This is a Tcl_Event structure that the Quit AppleEvent handler uses to * schedule the ReallyKillMe function. */ typedef struct KillEvent { Tcl_Event header; /* Information that is standard for all * events. */ Tcl_Interp *interp; /* Interp that was passed to the Quit * AppleEvent */ } KillEvent; /* * When processing an AppleEvent as an idle task, a pointer to one * of these structs is passed as the clientData. */ typedef struct AppleEventInfo { Tcl_Interp *interp; const char *procedure; Tcl_DString command; NSAppleEventDescriptor *replyEvent; /* Only used for DoScriptText. */ int retryCount; } AppleEventInfo; /* * Static functions used only in this file. */ static int MissedAnyParameters(const AppleEvent *theEvent); static int ReallyKillMe(Tcl_Event *eventPtr, int flags); static void ProcessAppleEvent(void *clientData); /* * Names of the procedures which can be used to process AppleEvents. */ static const char openDocumentProc[] = "::tk::mac::OpenDocument"; static const char launchURLProc[] = "::tk::mac::LaunchURL"; static const char printDocProc[] = "::tk::mac::PrintDocument"; static const char scriptFileProc[] = "::tk::mac::DoScriptFile"; static const char scriptTextProc[] = "::tk::mac::DoScriptText"; static const char getSdefProc[] = "::tk::mac::GetDynamicSdef"; #pragma mark TKApplication(TKHLEvents) @implementation TKApplication(TKHLEvents) - (void) terminate: (id) sender { (void)sender; [self handleQuitApplicationEvent:Nil withReplyEvent:Nil]; } - (void) superTerminate: (id) sender { (void) sender; [super terminate:nil]; } - (void) preferences: (id) sender { (void)sender; [self handleShowPreferencesEvent:Nil withReplyEvent:Nil]; } - (void) handleQuitApplicationEvent: (NSAppleEventDescriptor *)event withReplyEvent: (NSAppleEventDescriptor *)replyEvent { KillEvent *eventPtr; (void)event; (void)replyEvent; if (_eventInterp) { /* * Call the exit command from the event loop, since you are not * supposed to call ExitToShell in an Apple Event Handler. We put this * at the head of Tcl's event queue because this message usually comes * when the Mac is shutting down, and we want to kill the shell as * quickly as possible. */ eventPtr = (KillEvent *)ckalloc(sizeof(KillEvent)); eventPtr->header.proc = ReallyKillMe; eventPtr->interp = _eventInterp; Tcl_QueueEvent((Tcl_Event *) eventPtr, TCL_QUEUE_HEAD); } } - (void) handleOpenApplicationEvent: (NSAppleEventDescriptor *)event withReplyEvent: (NSAppleEventDescriptor *)replyEvent { (void)event; (void)replyEvent; if (_eventInterp && Tcl_FindCommand(_eventInterp, "::tk::mac::OpenApplication", NULL, 0)){ int code = Tcl_EvalEx(_eventInterp, "::tk::mac::OpenApplication", TCL_INDEX_NONE, TCL_EVAL_GLOBAL); if (code != TCL_OK) { Tcl_BackgroundException(_eventInterp, code); } } } - (void) handleReopenApplicationEvent: (NSAppleEventDescriptor *)event withReplyEvent: (NSAppleEventDescriptor *)replyEvent { (void)event; (void)replyEvent; [NSApp activateIgnoringOtherApps: YES]; if (_eventInterp && Tcl_FindCommand(_eventInterp, "::tk::mac::ReopenApplication", NULL, 0)) { int code = Tcl_EvalEx(_eventInterp, "::tk::mac::ReopenApplication", TCL_INDEX_NONE, TCL_EVAL_GLOBAL); if (code != TCL_OK){ Tcl_BackgroundException(_eventInterp, code); } } } - (void) handleShowPreferencesEvent: (NSAppleEventDescriptor *)event withReplyEvent: (NSAppleEventDescriptor *)replyEvent { (void)event; (void)replyEvent; if (_eventInterp && Tcl_FindCommand(_eventInterp, "::tk::mac::ShowPreferences", NULL, 0)){ int code = Tcl_EvalEx(_eventInterp, "::tk::mac::ShowPreferences", TCL_INDEX_NONE, TCL_EVAL_GLOBAL); if (code != TCL_OK) { Tcl_BackgroundException(_eventInterp, code); } } } - (void) handleOpenDocumentsEvent: (NSAppleEventDescriptor *)event withReplyEvent: (NSAppleEventDescriptor *)replyEvent { Tcl_Encoding utf8; const AEDesc *fileSpecDesc = nil; AEDesc contents; char URLString[1 + URL_MAX_LENGTH]; NSURL *fileURL; DescType type; Size actual; long count, index; AEKeyword keyword; Tcl_DString pathName; (void)replyEvent; /* * Do nothing if we don't have an interpreter. */ if (!_eventInterp) { return; } fileSpecDesc = [event aeDesc]; if (fileSpecDesc == nil ) { return; } /* * The AppleEvent's descriptor should either contain a value of * typeObjectSpecifier or typeAEList. In the first case, the descriptor * can be treated as a list of size 1 containing a value which can be * coerced into a fileURL. In the second case we want to work with the list * itself. Values in the list will be coerced into fileURL's if possible; * otherwise they will be ignored. */ /* Get a copy of the AppleEvent's descriptor. */ AEGetParamDesc(fileSpecDesc, keyDirectObject, typeWildCard, &contents); if (contents.descriptorType == typeAEList) { fileSpecDesc = &contents; } if (AECountItems(fileSpecDesc, &count) != noErr) { AEDisposeDesc(&contents); return; } /* * Construct a Tcl expression which calls the ::tk::mac::OpenDocument * procedure, passing the paths contained in the AppleEvent as arguments. */ AppleEventInfo *AEInfo = (AppleEventInfo *)ckalloc(sizeof(AppleEventInfo)); Tcl_DString *openCommand = &AEInfo->command; Tcl_DStringInit(openCommand); Tcl_DStringAppend(openCommand, openDocumentProc, TCL_INDEX_NONE); utf8 = Tcl_GetEncoding(NULL, "utf-8"); for (index = 1; index <= count; index++) { if (noErr != AEGetNthPtr(fileSpecDesc, index, typeFileURL, &keyword, &type, (Ptr) URLString, URL_MAX_LENGTH, &actual)) { continue; } if (type != typeFileURL) { continue; } URLString[actual] = '\0'; fileURL = [NSURL URLWithString:[NSString stringWithUTF8String:(char*)URLString]]; if (fileURL == nil) { continue; } (void)Tcl_ExternalToUtfDString(utf8, [[fileURL path] UTF8String], TCL_INDEX_NONE, &pathName); Tcl_DStringAppendElement(openCommand, Tcl_DStringValue(&pathName)); Tcl_DStringFree(&pathName); } Tcl_FreeEncoding(utf8); AEDisposeDesc(&contents); AEInfo->interp = _eventInterp; AEInfo->procedure = openDocumentProc; AEInfo->replyEvent = nil; AEInfo->retryCount = 0; if (Tcl_FindCommand(_eventInterp, "::tk::mac::OpenDocuments", NULL, 0)){ ProcessAppleEvent((void *)AEInfo); } else { Tcl_CreateTimerHandler(500, ProcessAppleEvent, (void *)AEInfo); } } - (void) handlePrintDocumentsEvent: (NSAppleEventDescriptor *)event withReplyEvent: (NSAppleEventDescriptor *)replyEvent { NSString* file = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; const char *printFile = [file UTF8String]; AppleEventInfo *AEInfo = (AppleEventInfo *)ckalloc(sizeof(AppleEventInfo)); Tcl_DString *printCommand = &AEInfo->command; (void)replyEvent; Tcl_DStringInit(printCommand); Tcl_DStringAppend(printCommand, printDocProc, TCL_INDEX_NONE); Tcl_DStringAppendElement(printCommand, printFile); AEInfo->interp = _eventInterp; AEInfo->procedure = printDocProc; AEInfo->replyEvent = nil; AEInfo->retryCount = 0; ProcessAppleEvent((void *)AEInfo); } - (void) handleDoScriptEvent: (NSAppleEventDescriptor *)event withReplyEvent: (NSAppleEventDescriptor *)replyEvent { OSStatus err; const AEDesc *theDesc = nil; DescType type = 0, initialType = 0; Size actual; char URLBuffer[1 + URL_MAX_LENGTH]; char errString[128]; /* * The DoScript event receives one parameter that should be text data or a * fileURL. */ theDesc = [event aeDesc]; if (theDesc == nil) { return; } err = AEGetParamPtr(theDesc, keyDirectObject, typeWildCard, &initialType, NULL, 0, NULL); if (err != noErr) { snprintf(errString, sizeof(errString), "AEDoScriptHandler: GetParamDesc error %d", (int)err); AEPutParamPtr((AppleEvent*)[replyEvent aeDesc], keyErrorString, typeChar, errString, strlen(errString)); return; } if (MissedAnyParameters((AppleEvent*)theDesc)) { snprintf(errString, sizeof(errString), "AEDoScriptHandler: extra parameters"); AEPutParamPtr((AppleEvent*)[replyEvent aeDesc], keyErrorString, typeChar,errString, strlen(errString)); return; } if (initialType == typeFileURL || initialType == typeAlias) { /* * This descriptor can be coerced to a file url. Construct a Tcl * expression which passes the file path as a string argument to * ::tk::mac::DoScriptFile. */ if (noErr == AEGetParamPtr(theDesc, keyDirectObject, typeFileURL, &type, (Ptr) URLBuffer, URL_MAX_LENGTH, &actual)) { if (actual > 0) { URLBuffer[actual] = '\0'; NSString *urlString = [NSString stringWithUTF8String:(char*)URLBuffer]; NSURL *fileURL = [NSURL URLWithString:urlString]; AppleEventInfo *AEInfo = (AppleEventInfo *)ckalloc(sizeof(AppleEventInfo)); Tcl_DString *scriptFileCommand = &AEInfo->command; Tcl_DStringInit(scriptFileCommand); Tcl_DStringAppend(scriptFileCommand, scriptFileProc, TCL_INDEX_NONE); Tcl_DStringAppendElement(scriptFileCommand, [[fileURL path] UTF8String]); AEInfo->interp = _eventInterp; AEInfo->procedure = scriptFileProc; AEInfo->replyEvent = nil; AEInfo->retryCount = 0; ProcessAppleEvent((void *)AEInfo); } } } else if (noErr == AEGetParamPtr(theDesc, keyDirectObject, typeUTF8Text, &type, NULL, 0, &actual)) { /* * The descriptor cannot be coerced to a file URL but can be coerced to * text. Construct a Tcl expression which passes the text as a string * argument to ::tk::mac::DoScriptText. */ if (actual > 0) { char *data = (char *)ckalloc(actual + 1); if (noErr == AEGetParamPtr(theDesc, keyDirectObject, typeUTF8Text, &type, data, actual, NULL)) { data[actual] = '\0'; AppleEventInfo *AEInfo = (AppleEventInfo *)ckalloc(sizeof(AppleEventInfo)); Tcl_DString *scriptTextCommand = &AEInfo->command; Tcl_DStringInit(scriptTextCommand); Tcl_DStringAppend(scriptTextCommand, scriptTextProc, TCL_INDEX_NONE); Tcl_DStringAppendElement(scriptTextCommand, data); AEInfo->interp = _eventInterp; AEInfo->procedure = scriptTextProc; AEInfo->retryCount = 0; if (Tcl_FindCommand(AEInfo->interp, AEInfo->procedure, NULL, 0)) { AEInfo->replyEvent = replyEvent; ProcessAppleEvent(AEInfo); } else { AEInfo->replyEvent = nil; ProcessAppleEvent(AEInfo); } } } } } - (void)handleURLEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent { NSString* url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; const char *cURL=[url UTF8String]; AppleEventInfo *AEInfo = (AppleEventInfo *)ckalloc(sizeof(AppleEventInfo)); Tcl_DString *launchCommand = &AEInfo->command; (void)replyEvent; Tcl_DStringInit(launchCommand); Tcl_DStringAppend(launchCommand, launchURLProc, TCL_INDEX_NONE); Tcl_DStringAppendElement(launchCommand, cURL); AEInfo->interp = _eventInterp; AEInfo->procedure = launchURLProc; AEInfo->replyEvent = nil; AEInfo->retryCount = 0; ProcessAppleEvent((void *)AEInfo); } - (void)handleGetSDEFEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { AppleEventInfo *AEInfo = (AppleEventInfo *)ckalloc(sizeof(AppleEventInfo)); Tcl_DString *sdefCommand = &AEInfo->command; (void)event; (void)replyEvent; Tcl_DStringInit(sdefCommand); Tcl_DStringAppend(sdefCommand, getSdefProc, TCL_INDEX_NONE); AEInfo->interp = _eventInterp; AEInfo->procedure = getSdefProc; AEInfo->replyEvent = nil; AEInfo->retryCount = 0; ProcessAppleEvent((void *)AEInfo); } @end #pragma mark - /* *---------------------------------------------------------------------- * * ProcessAppleEvent -- * * Usually used as an idle task which evaluates a Tcl expression generated * from an AppleEvent. If the AppleEventInfo passed as the client data * has a non-null replyEvent, the result of evaluating the expression will * be added to the reply. This must not be done when this function is * called as an idle task, but is done when handling DoScriptText events * when this function is called directly. * * Results: * None. * * Side effects: * The expression will be evaluated and the clientData will be freed. * The replyEvent may be modified to contain the result of evaluating * a Tcl expression. * *---------------------------------------------------------------------- */ static void ProcessAppleEvent( void *clientData) { int code; AppleEventInfo *AEInfo = (AppleEventInfo*) clientData; if (!AEInfo->interp) { return; } /* * Apple events that are delivered during the app startup can arrive * before the Tcl procedure for handling the events has been defined. * If the command is not found we create a timer handler to process * the event later, hopefully after the command has been created. * We retry up to 2 times before giving up. */ if (!Tcl_FindCommand(AEInfo->interp, AEInfo->procedure, NULL, 0)) { if (AEInfo->retryCount < 2) { AEInfo->retryCount++; Tcl_CreateTimerHandler(200, ProcessAppleEvent, clientData); } else { ckfree(clientData); } return; } code = Tcl_EvalEx(AEInfo->interp, Tcl_DStringValue(&AEInfo->command), Tcl_DStringLength(&AEInfo->command), TCL_EVAL_GLOBAL); if (AEInfo->replyEvent && code >= 0) { Tcl_Size reslen; const char *result = Tcl_GetStringFromObj(Tcl_GetObjResult(AEInfo->interp), &reslen); if (code == TCL_OK) { AEPutParamPtr((AppleEvent*)[AEInfo->replyEvent aeDesc], keyDirectObject, typeChar, result, reslen); } else { AEPutParamPtr((AppleEvent*)[AEInfo->replyEvent aeDesc], keyErrorString, typeChar, result, reslen); AEPutParamPtr((AppleEvent*)[AEInfo->replyEvent aeDesc], keyErrorNumber, typeSInt32, (Ptr) &code, sizeof(int)); } } else if (code != TCL_OK) { Tcl_BackgroundException(AEInfo->interp, code); } Tcl_DStringFree(&AEInfo->command); ckfree(clientData); } /* *---------------------------------------------------------------------- * * TkMacOSXInitAppleEvents -- * * Register AppleEvent handlers with the NSAppleEventManager for * this NSApplication. * * Results: * None. * * Side effects: * None. * *---------------------------------------------------------------------- */ void TkMacOSXInitAppleEvents( TCL_UNUSED(Tcl_Interp *)) { NSAppleEventManager *aeManager = [NSAppleEventManager sharedAppleEventManager]; static Boolean initialized = FALSE; if (!initialized) { initialized = TRUE; [aeManager setEventHandler:NSApp andSelector:@selector(handleQuitApplicationEvent:withReplyEvent:) forEventClass:kCoreEventClass andEventID:kAEQuitApplication]; [aeManager setEventHandler:NSApp andSelector:@selector(handleOpenApplicationEvent:withReplyEvent:) forEventClass:kCoreEventClass andEventID:kAEOpenApplication]; [aeManager setEventHandler:NSApp andSelector:@selector(handleReopenApplicationEvent:withReplyEvent:) forEventClass:kCoreEventClass andEventID:kAEReopenApplication]; [aeManager setEventHandler:NSApp andSelector:@selector(handleShowPreferencesEvent:withReplyEvent:) forEventClass:kCoreEventClass andEventID:kAEShowPreferences]; [aeManager setEventHandler:NSApp andSelector:@selector(handleOpenDocumentsEvent:withReplyEvent:) forEventClass:kCoreEventClass andEventID:kAEOpenDocuments]; [aeManager setEventHandler:NSApp andSelector:@selector(handlePrintDocumentsEvent:withReplyEvent:) forEventClass:kCoreEventClass andEventID:kAEPrintDocuments]; [aeManager setEventHandler:NSApp andSelector:@selector(handleDoScriptEvent:withReplyEvent:) forEventClass:kAEMiscStandards andEventID:kAEDoScript]; [aeManager setEventHandler:NSApp andSelector:@selector(handleURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; /* * We do not load our sdef dynamically but this event handler * is required to silence error messages from inline execution * of AppleScript at the Objective-C level. */ [aeManager setEventHandler:NSApp andSelector:@selector(handleGetSDEFEvent:withReplyEvent:) forEventClass:'ascr' andEventID:'gsdf']; } } /* *---------------------------------------------------------------------- * * TkMacOSXDoHLEvent -- * * Dispatch an AppleEvent. * * Results: * None. * * Side effects: * Depend on the AppleEvent. * *---------------------------------------------------------------------- */ int TkMacOSXDoHLEvent( void *theEvent) { /* According to the NSAppleEventManager reference: * "The theReply parameter always specifies a reply Apple event, never * nil. However, the handler should not fill out the reply if the * descriptor type for the reply event is typeNull, indicating the sender * does not want a reply." * The specified way to build such a non-nil descriptor is used here. But * on OSX 10.11, the compiler nonetheless generates a warning. I am * supressing the warning here -- maybe the warnings will stop in a future * compiler release. */ #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnonnull" #endif NSAppleEventDescriptor* theReply = [NSAppleEventDescriptor nullDescriptor]; NSAppleEventManager *aeManager = [NSAppleEventManager sharedAppleEventManager]; return [aeManager dispatchRawAppleEvent:(const AppleEvent*)theEvent withRawReply: (AppleEvent *)theReply handlerRefCon: (SRefCon)0]; #ifdef __clang__ #pragma clang diagnostic pop #endif } /* *---------------------------------------------------------------------- * * ReallyKillMe -- * * This procedure tries to kill the shell by running exit, called from * an event scheduled by the "Quit" AppleEvent handler. * * Results: * Runs the "exit" command which might kill the shell. * * Side effects: * None. * *---------------------------------------------------------------------- */ static int ReallyKillMe( Tcl_Event *eventPtr, TCL_UNUSED(int)) { Tcl_Interp *interp = ((KillEvent *) eventPtr)->interp; int quit = Tcl_FindCommand(interp, "::tk::mac::Quit", NULL, 0)!=NULL; if (!quit) { Tcl_Exit(0); } int code = Tcl_EvalEx(interp, "::tk::mac::Quit", TCL_INDEX_NONE, TCL_EVAL_GLOBAL); if (code != TCL_OK) { /* * Should be never reached... */ Tcl_BackgroundException(interp, code); } return 1; } /* *---------------------------------------------------------------------- * * MissedAnyParameters -- * * Checks to see if parameters are still left in the event. * * Results: * True or false. * * Side effects: * None. * *---------------------------------------------------------------------- */ static int MissedAnyParameters( const AppleEvent *theEvent) { DescType returnedType; Size actualSize; OSStatus err; err = AEGetAttributePtr(theEvent, keyMissedKeywordAttr, typeWildCard, &returnedType, NULL, 0, &actualSize); return (err != errAEDescNotFound); } /* * Local Variables: * mode: objc * c-basic-offset: 4 * fill-column: 79 * coding: utf-8 * End: */