cocoa open multiple files with associated app - objective-c

I have an array of filenames that i would like to pass to an external application for opening. I'd like to do one of the following:
a) Somehow instruct OSX to open all these files with an associated application, but it must invoke the target app's openFiles NSApplication delegate method
b) Specify the application to open these files with (and also invoke openFiles)
Basically it doesn't matter which solution to realise, because these files will be associated with the target application anyway. How would i do one of these things?

To open a whole bunch of files at once, send the shared NSWorkspace object an openURLs:withAppBundleIdentifier:options:additionalEventParamDescriptor:launchIdentifiers: message, or call the LSOpenURLsWithRole function or the LSOpenFromURLSpec function. Either way, you'll pass an array of URLs to items to open.
Each one of these will let you identify a specific application to use. NSWorkspace lets you specify it by bundle identifier, while the two Launch Services functions let you provide the URL or FSRef to a specific application bundle.
… it must invoke the target app's openFiles NSApplication delegate method
That isn't possible to require, because (a) the application may be document-based, in which case it probably does not have an NSApplication delegate and, even if it does, such a delegate would probably not respond to application:openFiles:, and (b) the application may not be Cocoa-based, in which case it would handle the Open Documents Apple Event directly. None of this is your application's business, so don't worry about it.

First add your video files to Resources Folder.The code like follow:
NSString* bundlePath = [[NSBundle mainBundle] bundlePath];
NSString* videoPath = [bundlePath stringByAppendingPathComponent:#"/Contents/Resources/video.mov"];
[[NSWorkspace sharedWorkspace] openFile:videoPath];

Related

macOS / Cocoa: How to get the path of the running app's main bundle after the user moves it on disk?

In macOS apps, if you need to get the path of your app or any resources in its bundle, you can of course use the mainBundle static method of NSBundle:
NSBundle *bundle = [NSBundle mainBundle];
NSString *appPath = [bundle bundlePath];
NSString *resourceFolderPath = [bundle resourcePath];
... etc ...
However, if the user moves your app's bundle to a different location on disk while the app is running, then this method will not work. All of the above functions will still return the bundle's old paths from before the user moved it.
How can you get the current path of your bundle or the path of any resource inside of it regardless of whether the user has moved it on disk?
So far, I've tried using NSRunningApplication to get its current location like so:
NSRunningApplication *thisApp = [NSRunningApplication runningApplicationWithProcessIdentifier:getpid()];
NSLog(#"bundle URL: %#", [thisApp bundleURL]);
...but that too returns the old path of the bundle!
Moving an application that's already running typically isn't supported. If that's a scenario you really need to support then you can do the following:
When the app is first launched, get the app's current path as you would normally and then create an NSURL bookmark for that path.
Whenever you need to know the app's current path you can resolve the NSURL bookmark you created at launch. The bookmark will resolve to app's current path, even if the app has been moved.
More info on NSURL bookmarks is available here: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/AccessingFilesandDirectories/AccessingFilesandDirectories.html#//apple_ref/doc/uid/TP40010672-CH3-SW10
While macOS doesn't care if you move documents around as they're open, it does care if you move applications around. This is a long-recognized issue in macOS development.
Instead of trying to fight it, app developers tend to either not care, or ask users if they want to move the application to the Applications directory at launch (since this is the most common move destination for apps). LetsMove is a project that demonstrates how you would do that.

Access contents of Watchkit extension's host's mainBundle

I'm writing a WatchKit extension and I'd like to read a file out of the host application's [NSBundle mainBundle]. I've tried [NSBundle bundleWithIdentifier:] but that just returns nil.
I have several potential workarounds, but nothing that would be as simple as "just read what you need from the host's mainBundle".
Is there a way of doing this?
The host app and your WatchKit extension can share files in only one of two ways, as far as I know:
Shared app group
Including a file in both targets
They run in separate processes and aren't accessible to each other outside of approved methods.
I ran into a similar problem like yours. The main host app has a particular pList that I needed to read, and I couldn't read from watch extension because they are isolated.
So in the watch I invoked the openParentApplication method
and in the main application my handler was something along the lines of
-(void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply
{
NSString *request = [userInfo objectForKey:#"request"];
if ([request isEqualToString:ReadFile])
{
//read the file. and then i like to put it into a NSDictionary
NSDictionary *responseDictionary = //whatever
reply(responseDictionary);
}
else{ reply(nil); }
}
And then the contents were returned to me in the callback closure on the watch of the openParentApplication. Seems to work. Though your situation could be different in which case this method might not be viable.
From the Apple WatchKit programming guide:
To share preferences data between apps, create an NSUserDefaults object using the identifier of the shared group. The initWithSuiteName: method of NSUserDefaults creates an object that allows access to the shared user defaults data. Both processes can access this data and write changes to it.
Your main app can write a NSDictionary/NSArray to the shared prefs, and then the watch kit can pull it out, without starting the main app - however, the main app will have to be run at least once to update the shared prefs.

Filter [NSWorkspace runningApplications] to contain only user applications (no daemons/UIAgents)

Is there a way to filter the list of applications given by [NSWorkspace runningApplications] to hide all daemons, etc short of manually checking each application's plist for the UIAgent key? If an application doesn't show in the dock, I'm not interested in it.
UPDATE: If you’re using Objective-C, my original answer below still applies, but if you’re using Swift this can very easily be performed more cleanly, thanks to Swift’s filter function for collection types.
let workspace = NSWorkspace.sharedWorkspace()
let apps = workspace.runningApplications.filter { (app) -> Bool in
return app.activationPolicy == .Regular
};
In Objective-C something similar can be done with NSArray’s various predicate-based and enumeration methods, but they’ll be a little more long-winded than their Swift counterpart.
I found the answer after some searching, but it's something that might not be immediately obvious.
An easy way to only grab processes which have icons in the Dock is by doing a simple fast enumeration loop and checking each NSRunningApplication's activationPolicy, like so:
NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
NSArray *apps = [workspace runningApplications];
for (NSRunningApplication *a in apps) {
if (a.activationPolicy == NSApplicationActivationPolicyRegular) {
// Do stuff here
}
}
Typically, applications with normal windows and dock icons use NSApplicationActivationPolicyRegular. Menu extras and Alfred-type applications use NSApplicationActivationPolicyAccessory. Daemons, etc with no user visibility whatsoever use NSApplicationActivationPolicyProhibited. These constants correspond with the LSUIElement and LSBackgroundOnly keys in each application's Info.plist.
This approach should catch applications which have settings that allow the user to toggle the presence of the application's dock icon through setting their activationPolicy dynamically.

Couple service related "bugs"

I've written an application that can be invoked as a service (by right-clicking a file in Finder and selecting to open it with my application), but there are a couple of unwanted side-effects when doing this.
Example of service target method:
- (void)doSomething:(NSPasteboard *)pboard userData:(NSString *)userData error:(NSString **)error {
NSArray *files = [pboard propertyListForType:NSFilenamesPboardType];
NSLog(#"Selected file(s): %#", files);
[self.anotherWindow makeKeyAndOrderFront:self];
}
1) When the application is launched this way (even if already open in debug mode), I seem unable to access other windows/controls from the doSomething function. The above attempt to show "anotherWindow", for example, produces no errors, but doesn't "do" anything. According to the stack trace, when inspected from -doSomething, all gui components have values 0x0000000000000000 - yet the application is displayed and fully functional. It's only from -doSomething that I cannot reach them. "self" also has a different value when inspected from -doSomething versus -applicationDidFinishLaunching. I'm not sure how or why -doSomething is acquiring a different self/AppDelegate with uninitialized components. Seemingly fixed by [NSApp setServicesProvider:self];
2) I am not clear on how the system decides which copy of the application to launch when the service is invoked, but it usually doesn't pick the one I want. I have a copy in /Debug, a copy in /Release, a copy on my desktop... and if I delete one, it opens the file with another one instead (some sort of fallback-chain?). How do I configure the service (in code or thru .plist) to open a specific version/location of this app? But this is a dev machine. If I release a distributable which installs to /Applications, do I ever really need to worry about this?
1) Double-check your XIB to makes sure that you've got everything hooked up correctly and then try launching the app with a breakpoint set at the NSLog above and verify that self.anotherWindow points at what you want. If, for some reason, the breakpoint isn't firing, trying adding an:
NSLog( #"Window: %#", self.anotherWindow);
To make sure everything is initialized and hooked up
2) The system uses Launch Services to determine which version of the application to launch. Often it is the version most recently added to the system (which will cause the Launch Services database to be modified), but it is possible, depending on how your system is configured, that it won't be the version you expect.
You can manually inquire and modify the launch services database using:
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister
(yeah, really long path). If you use the -dump option, that'll give you all of the data in the system (pipe into a file and search through it to get a better idea what's going on). If you search of the bundle id, you'll see all of the entries for the app. Generally, most recent wins, but you can force a reload (instructions below).
If you just want to force a reload based on a a particular binary, use the -f flag and the path to the application:
..../lsregister -f /Applications/Foo.app
You can also use -u to explicitly unregister something.
Hopefully this will give you an idea what's going on here.

Opening a file using ScriptingBridge

I have an AppleScript that I am trying to convert to ScriptingBridge. Since my application is a C++/Obj-C application, ScriptingBridge is much easier to use and quite a bit faster (not to mention I hate dynamically building AppleScripts).
The AppleScript sends a message to Photoshop to open a file. The file parameter is sent as an alias, but ScriptingBridge imports the parameter as an id. I don't know what Obj-C object I should pass in?
I've tried passing an NSURL and an NSString (probably incorrectly :-P), but to no avail. Any suggestions on what I should be passing for the file alias?
The short answer is that you can't open documents in Photoshop with Scripting Bridge.
Apple's docs really spell it out like it is. All classes must have a container, which is a mutable array, that they need to be added to before they can be acted upon, as shown in the generated header...
#interface photoshopCS4Application : SBApplication
- (SBElementArray *) documents;
- (SBElementArray *) fonts;
- (SBElementArray *) notifiers;
... and that is the complete list of top-level containers available to us. The open command requires a photoshopCS4OpenOptions to be generated and populated. Because the API doesn't expose the array to store the newly created photoshopCS4OpenOptions object, we can't use a newly created photoshopCS4OpenOptions object. Therefore we can't make a target document and by extensions can't use the open command in Scripting Bridge. The same can be said of all the commands that require some kind of options object.
The only workaround that I have sorted out is to either open a document with native Applescript called from Cocoa or objc-appscript, and then parse the documents array looking for the one just opened. It's not ideal, but then neither is Scripting Bridge because it requires application developers write their scripting APIs in a very specific way that is not native to the OSA framework.
If your program is such that opening a Photoshop document can be executed outside your AppleScript script/Scripting Bridge code, Cocoa provides a method to open files with a specific application:
[[NSWorkspace sharedWorkspace] openFile:#"/Users/bavarious/Desktop/test.psd" withApplication:#"Adobe Photoshop CS4"];
or, if you want to use the default application that handles that file type, you can drop the application name altogether:
[[NSWorkspace sharedWorkspace] openFile:#"/Users/bavarious/Desktop/test.psd"];
Consider Appscript. http://appscript.sourceforge.net/
Here's the code using that:
APApplication *adobePhotoshopCs4 = [APApplication applicationWithName: #"Adobe Photoshop CS4"];
id result = [[adobePhotoshopCs4 open_] send];
(Note, I'm not a Cocoa programmer - I mainly use Appscript with Python but Appscript comes with ASTranslate which translates Applescript into Python, Ruby or Obj-C and that's the output - but I've found there are subtle mistakes in the past sometimes with the translator)