2

NSWorkspace has the method open(_:withAppBundleIdentifier: [...] ):

Opens one or more files from an array of URLs.

func open(_ urls: [URL], 
     withAppBundleIdentifier bundleIdentifier: String?, 
     options: NSWorkspace.LaunchOptions = [], 
     additionalEventParamDescriptor descriptor: NSAppleEventDescriptor?, 
     launchIdentifiers identifiers:) -> Bool

The NSApplicationDelegate of the app you want to open has corresponding methods that are called to open the URL(s) you provide:

func application(_ sender: NSApplication, openFile filename: String) -> Bool
func application(_ sender: NSApplication, openFiles filenames: [String])

Back to open(_:withAppBundleIdentifier: [...]), that method has an NSAppleEventDescriptor parameter:

additionalEventParamDescriptor descriptor: NSAppleEventDescriptor?
Additional options specified in an AppleEvent-style descriptor. For example, you could use this parameter to specify additional documents to open when the app is launched.


I would like to send additional information to the app that will open the files.

This would be used similarly to the userInfo dictionary on a notification.

I've constructed a NSAppleEventDescriptor object to represent this information. I can set this event descriptor in the NSWorkspace open( ... ) function.

But how do I receive this event descriptor in Application Delegate of the target app?

The application(_: openFile:) functions have no parameters for the event descriptors or any other "userInfo"-type additional information.


Code

Based on answers and other questions, I settled on the solution below. I am now getting a triggered handler for Apple Events. But the Apple Event that I am setting on the NSWorkspace function is not the one that is received in the handler! How do I get my Apple Event instead?


Send

let appleEvent = NSAppleEventDescriptor(eventClass:       AEEventClass(kCoreEventClass),
                                        eventID:          AEEventID(kAEOpenDocuments),
                                        targetDescriptor: nil,
                                        returnID:         AEReturnID(kAutoGenerateReturnID),
                                        transactionID:    AETransactionID(kAnyTransactionID))
appleEvent.setDescriptor(NSAppleEventDescriptor(string: "THIS IS A TEST"), forKeyword: keyDirectObject)

let didOpen = AppKit.NSWorkspace.shared.open([URL(fileURLWithPath: "/path/image.png")],
                                             withAppBundleIdentifier: bundleID,
                                             options: [.withErrorPresentation],
                                             additionalEventParamDescriptor: appleEvent,
                                             launchIdentifiers: nil)

Sent Apple Event:

<NSAppleEventDescriptor: 'aevt'\'odoc'{ '----':'utxt'("THIS IS A TEST") }>


Receive

class AppDelegate: NSObject, NSApplicationDelegate {
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        NSAppleEventManager.shared().setEventHandler(self,
                                                     andSelector: #selector(handle(event:replyEvent:)),
                                                     forEventClass: AEEventClass(kCoreEventClass),
                                                     andEventID: AEEventID(kAEOpenDocuments))
    }
    
    @objc func handle(event: NSAppleEventDescriptor?, replyEvent: NSAppleEventDescriptor?) {
        guard let event = event,
            event.eventClass == AEEventClass(kCoreEventClass) && event.eventID == AEEventID(kAEOpenDocuments) else {
            return
        }

        guard let additionalEventParamDescriptor = event.paramDescriptor(forKeyword: keyAEPropData) else {
            return
        }

        
        guard let directObject = additionalEventParamDescriptor.paramDescriptor(forKeyword: keyDirectObject) else {
            return
        }
        
        print(directObject)
    }
    
}

Received Apple Event:

<NSAppleEventDescriptor: 'aevt'\'odoc'{ '----':[ 'bmrk'(888/$626F6F6B7803000000000 [....] 00000AC01000000000000...$) ] }>

Community
  • 1
  • 1
pkamb
  • 33,281
  • 23
  • 160
  • 191
  • https://stackoverflow.com/questions/5812371/how-can-a-mac-app-determine-the-method-used-to-launch-it/5812685 – pkamb Aug 14 '19 at 18:21
  • https://www.barebones.com/support/develop/odbsuite.html >>> *When doing so, the server adds an additional parameter to the `odoc` event.* << – pkamb Aug 19 '19 at 20:59
  • *"Your application will only receive Apple events that target it—that is, events that specify your application in their target address descriptor."* https://applescriptlibrary.files.wordpress.com/2013/11/apple-events-programming-guide.pdf – pkamb Aug 20 '19 at 00:08
  • Seeing console log `AppleEvents failed to encode extension for /Users/user/Desktop, err=Operation not permitted/1` - which is one of the destinations I'm sending in the Apple Script... – pkamb Aug 20 '19 at 01:57
  • *Don't bother passing an additionalEventParamDescriptor. It's hard to get right, and I've seen evidence that it isn't even passed properly. (I've dropped down to direct use of Launch Services, which did pass the parameter correctly.)* https://lists.apple.com/archives/cocoa-dev/2009/Apr/msg01702.html – pkamb Aug 20 '19 at 02:49
  • Posted to Apple Developer Forums: https://forums.developer.apple.com/message/377852 – pkamb Aug 20 '19 at 03:12
  • The `~'prdt'` part seems to be [passThruParams](https://developer.apple.com/documentation/coreservices/lslaunchfsrefspec/1445933-passthruparams) *A pointer to an Apple event descriptor that is passed untouched as an optional parameter, with keyword `keyAEPropData ('prdt')`, in the Apple event sent to each application launched or activated.* – pkamb Aug 21 '19 at 20:54
  • twitter discussion: https://twitter.com/catlan/status/1164242863066112000 – pkamb Aug 21 '19 at 23:45
  • Created a sample project: https://github.com/pkamb/Feedback_AdditionalEventParamDescriptor – pkamb Aug 26 '19 at 22:38
  • I opened a DTS ticket for this. Adding an Answer here based on that ticket is on my TODO list, ping me if you need it. – pkamb Mar 14 '20 at 19:37

1 Answers1

2

kAEOpenDocuments events approach

- (void)applicationWillFinishLaunching:(NSNotification *)notification {
    [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(handleAppleEvent:withReplyEvent:) forEventClass:kCoreEventClass andEventID:kAEOpenDocuments];
}

- (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent {

}

-[NSWorkspace openURLs: ...] generates an kAEOpenDocuments event that contains the URLs as sandbox save bookmark data. (See +[NSURL URLByResolvingBookmarkData: options: relativeToURL: bookmarkDataIsStale: error:]).

The additionalEventParamDescriptor:

When creating the additionalEventParamDescriptor with kAEOpenDocuments with a custom parameters, this event seems to get merged with the underlaying kAEOpenDocuments event from -[NSWorkspace openURLs: ...].

NSAppleEventDescriptor *targetDescriptor = nil;
NSAppleEventDescriptor *appleEvent = nil;

targetDescriptor = [NSAppleEventDescriptor descriptorWithDescriptorType:typeApplicationBundleID
                                                                   data:targetBundleID];
appleEvent = [NSAppleEventDescriptor appleEventWithEventClass:kCoreEventClass
                                                      eventID:kAEOpenDocuments
                                             targetDescriptor:targetDescriptor
                                                     returnID:kAutoGenerateReturnID
                                                transactionID:kAnyTransactionID];
[appleEvent setParamDescriptor:[NSAppleEventDescriptor descriptorWithString:@"You're so good looking"]
                    forKeyword:'urln'];

[[NSWorkspace sharedWorkspace] openURLs:@[ [[NSBundle mainBundle] resourceURL] ]
                withAppBundleIdentifier:bundleIdentifier
                                options:NSWorkspaceLaunchNewInstance
         additionalEventParamDescriptor:appleEvent
                      launchIdentifiers:NULL];

Sample lldb output:

NSAppleEventDescriptor: 'aevt'\'odoc'{ ~'prdt':'aevt'\'odoc'{ 'urln':'utxt'("You're so good looking") }, '----':[ 'bmrk'(1432/$626F6F6B980 ...) }

Note: When setting the NSAppleEventManager for kAEOpenDocuments this overwrites AppKits build-in functionality of the application:openFile: or application:openFiles: methods. The custom event handler needs to implement all that.

custom events approach

Based on my findings sending a custom event class with a custom event ID does not trigger the event handler. ¯_(ツ)_/¯

catlan
  • 25,100
  • 8
  • 67
  • 78
  • I'm using `eventClass: AEEventClass(kCoreEventClass), eventID: AEEventID(kAEOpenDocuments)`. I get the callback, but it oddly seems to be a completely different event in the handler. What Class/ID should I use? Or is there some other reason why the event is changed in transit? – pkamb Aug 17 '19 at 01:20
  • From what I saw you get the kAEOpenDocuments event, not the additional event. Which makes sense. But I also wasn't able to get a custom event handler to trigger on the additional event. – catlan Aug 19 '19 at 10:50
  • I updated my answer, never did what you are trying to do myself. Only remembered playing with something related a while ago to thought I give it a try. – catlan Aug 19 '19 at 10:50
  • The issue is that my handler is triggered even if I DO NOT include the `additionalEventParamDescriptor:` in my NSWorkspace function call. In both cases, in the handler, the event appears as a "bookmark" event with the direct object: ` – pkamb Aug 19 '19 at 19:41
  • 1
    Hope the updated answer clears things up... it's still gonna require work with parsing the kAEOpenDocuments event. – catlan Aug 20 '19 at 07:07
  • I see what you're saying, and I'm expecting the Apple Events to "merge". My code is nearly identical to the code in your answer (and I'm about to try your code exactly). But I'm still NEVER seeing my custom Apple Event in the handled event; only the built-in bookmark events :/ – pkamb Aug 20 '19 at 19:05
  • Discrepancy seems to be in the URLs being opened. If I open `[Bundle.main.resourceURL!]` (like your example) I finally DO see my custom Apple event. If I use the file URLs I actually want to open, I do not see my custom Apple event in the handler... – pkamb Aug 20 '19 at 19:22
  • Hm, is your app sandboxed or not? – catlan Aug 20 '19 at 20:31
  • Yes it IS sandboxed. – pkamb Aug 20 '19 at 20:46
  • Hm. My test is sandboxed as well. I use NSOpenPanel URL to pass through. macOS 10.14.6. – catlan Aug 21 '19 at 14:24
  • example: https://www.dropbox.com/s/rtnzj5hlugekczw/AppleEventTest%2020.57.17.zip?dl=0 – catlan Aug 21 '19 at 14:24
  • I switched your code to use `NSWorkspaceLaunchWithErrorPresentation` rather than `NSWorkspaceLaunchNewInstance` for easier debugging / breakpoints. When I select a `.JPG` the event contains the `additionalEventParamDescriptor`. When I select a `.png` or `.pdf` it does NOT contain the extra event. Testing now, this corresponds with my app. The same `.JPG` does work! But the `.png` file I had been testing with does not. – pkamb Aug 21 '19 at 18:14
  • Hm. Just tried it with a png and it did contain the extra parameter. NSWorkspaceLaunchNewInstance: I start one app instance myself, then Xcode run with "Waiting for executable to be launched" and then press the button. – catlan Aug 21 '19 at 18:23
  • Could any of you please add the missing code that parses the apple-event and extracts the additionalEventParamDescriptor from it? I need this, and can't find anywhere a reasonable explanation on how to build such code. – Motti Shneor May 24 '21 at 09:25
  • @MottiShneor that code is at the bottom of my original Question. – pkamb May 25 '21 at 19:42