I'm trying to use the new methods for NSOpenPanel and set its initial directory. The problem is that it only works at the first time and after that it just "remembers" the last selected folder, which I don't want. I have to use the depreciated runModalForDirectory:file: to make it work. It's less than ideal because it was deprecated at 10.6, but thankfully it still works on Lion.
My code is:
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setAllowedFileTypes:[NSArray arrayWithObjects: #"jpg",#"JPG",#"png", nil]];
panel.canChooseDirectories = YES;
panel.allowsMultipleSelection = YES;
handler = ^(NSInteger result) {stuff};
[panel setDirectoryURL:[NSURL URLWithString:#"/Library/Desktop Pictures"]];
There are a couple things to look into:
~/Pictures is not a valid URL. file:///Users/user/Pictures is. -[NSURL URLWithString:] requires a valid URL. You probably want to use -[NSURL fileURLWithPath:] instead. It will turn /Users/user/Pictures into file:///Users/user/Pictures.
Tildes are not automatically expanded, so you want to use [#"~/Pictures stringByExpandingTildeInPath] to get an actual file path.
Put together, change the last line to:
[panel setDirectoryURL:[NSURL fileURLWithPath:[#"~/Pictures" stringByExpandingTildeInPath]]];
I think that should work.
The panel in Lion expects an URL like: file://localhost/Library/Desktop Pictures, but your URL starts with the actual path.
Use [NSURL fileURLWithPath:#"/Library/Desktop Pictures"] instead.
Happy coding!
Related
So I'm new to Cocoa and Objective-C programming, but I've read enough to be able to modify this one program I'm working on almost completely to my satisfaction...with the exception of one particular use case.
The program lets a user manually specify a path for where they want the files downloaded to reside, which is then saved into user defaults dictionary through binding the label's value to user defaults controller. The label's stringValue is then updated through the IBAction attached to the 'Open in Finder' button (by modifying the label outlet's stringValue inside the Push button's IBAction) which gets triggered whenever the user clicks the button and chooses a new path.
My problem is that when it comes time to download a file, if the path that the user has chosen no longer exists or is not valid, it will default to the user's desktop. But when this is the case, I do not see the desktop path being reflected in the label. This makes sense too, because there is no code to directly modify the label's stringValue in that case (no access to the label's outlet from that file). I've been trying to come up with a solution to fix this little thing that's been bugging me, but I haven't found a solution that's worked yet. Any advice or tips would be greatly welcome! I've included snippets of what the relevant pieces of my code look like below.
"PreferencesController.m"
- (IBAction)onOpenDownloadsPath:(id)sender {
NSOpenPanel* panel = [NSOpenPanel openPanel];
[panel setCanChooseFiles:NO];
[panel setCanChooseDirectories:YES];
[panel setAllowsMultipleSelection:NO];
[panel beginWithCompletionHandler:^(NSInteger result){
if (result == NSFileHandlingPanelOKButton) {
NSURL* theDir = [[panel URLs] objectAtIndex:0];
NSString* thePath = theDir.path;
[Preferences setDownloadsPath:thePath];
self->_labelDisplay.stringValue = [Preferences getDownloadsPath];
}
}];}
"Preferences.m"
+ (void)setDownloadsPath:(NSString *)value {
NSUserDefaults* ud = [NSUserDefaults standardUserDefaults];
[ud setObject:value forKey:#"Preferences.downloadsPath"];
}
+ (NSString*)getDownloadsPath {
NSUserDefaults* ud = [NSUserDefaults standardUserDefaults];
return [ud stringForKey:#"Preferences.downloadsPath"];
}
Solved. I used an NSNotification observer/listener to listen for when the set function would get called, then updated the stringValue in the label accordingly.
I am trying to get an NSOpenPanel to do the following:
Cannot select files
Can select directories and packages
Cannot see package contents
In order to get the first 2 points I need to use:
[openDlg setCanChooseFiles:NO];
[openDlg setCanChooseDirectories:YES];
[openDlg setTreatsFilePackagesAsDirectories:YES];
However this means that when in column view and a package is selected, the contents of the package are shown. I want the behaviour which occurs when we have [openDlg setCanChooseFiles:YES]; [openDlg setTreatsFilePackagesAsDirectories:NO]; i.e. the package can be selected but the column view browser does not show the contents when it is selected.
Any ideas?
There's a now deprecated method in the NSSavePanel's delegate with a method name of:
- (BOOL) panel: (id) sender shouldShowFilename: (NSString *) filename]
which can be used to tell the save panel to not display certain filenames.
Details about how to use it can be seen in this Apple QA technote, which details how to do exactly the opposite of what you are trying to do (their example is how to choose any file but ignore packages, but you may be able to flip the internal logic around).
Now, remember that I said that the method is "deprecated". The NSSavePanel header file says this:
/* This method is deprecated in 10.6, and will be formally deprecated */
/* in a future release. Use panel:shouldEnableURL: instead */
- (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename;
What NSOpenSavePanelDelegate's panel:shouldEnableURL: apparently does it merely allow or disallow the file from being selectable.
To future-proof your app, you may need to do the respondsToSelector trick to make sure "shouldShowFilename" is still available as an option before using the less desirable "shouldEnableURL" method.
I implemented the NSOpenPanelDelegate method panel:shouldEnableURL: as follows:
- (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url {
BOOL showObject = NO;
// This checks if the path is a directory
[[NSFileManager defaultManager] fileExistsAtPath:[url path] isDirectory:&showObject];
// This checks if the path is a package
if ([[NSWorkspace sharedWorkspace] isFilePackageAtPath:[url path]]) {
showObject = YES;
}
return showObject;
}
This doesn't require any further configuration (like setCanChooseDirectories:) and does exactly what I want!
I'm developing a sandboxed Mac App Store app which asks the user where to save files it downloads from elsewhere. I have this code to get the folder from the user (stripping out some error checking):
NSOpenPanel* openPanel = [NSOpenPanel openPanel];
[openPanel setAllowsMultipleSelection:NO];
[openPanel setCanChooseDirectories:YES];
[openPanel setResolvesAliases:YES];
NSInteger result = [openPanel runModal];
NSArray* urls = [openPanel URLs];
NSURL* folderURL = [urls objectAtIndex:0];
NSError* error;
NSData* bookmakeData = [folderURL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error];
_saveFolderBookmark = bookmakeData;
and when it comes time to move a file into this folder, I have this code:
BOOL isStale;
NSError* error;
NSURL* saveFolder = [NSURL URLByResolvingBookmarkData:_saveFolderBookmark
options:NSURLBookmarkCreationWithSecurityScope
relativeToURL:nil
bookmarkDataIsStale:&isStale
error:&error];
BOOL success = [saveFolder startAccessingSecurityScopedResource];
// Move the file somewhere else
NSWorkspace* workspace = [NSWorkspace sharedWorkspace];
NSInteger operationTag;
BOOL copied = [workspace performFileOperation:NSWorkspaceMoveOperation
source:[[self getDocumentsFolder] path]
destination:[saveFolder path]
files:[NSArray arrayWithObject:filename]
tag:&operationTag];
[saveFolder stopAccessingSecurityScopedResource];
Which is a lot of code to list to say that startAccessingSecurityScopedResource never returns success for me, either immediately after getting the ULR from NSOpenPanel or in a later run, with the bookmark data being saved in NSUserDefaults.
In the entitlements file, amongst other items, I have:
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
Is there anything wrong with this code?
This one is very easy. You're resolving with NSURLBookmarkCreationWithSecurityScope instead of NSURLBookmarkResolutionWithSecurityScope. Just change that line and it should work.
(It may not work during the same session where you create the bookmark, when you already have access to the URL, because you're not supposed to call it in that case. But on later runs it should, of course, because that's the whole point.)
The correct solution has been given above, but I thought I’d add another possibility for those who are having the same symptom (startAccessingSecurityScopedResource never returning YES). Make sure that you are using the actual URL returned by Powerbox, which can (probably will) be different than the URL of the actual resource you are trying to access. For example, Powerbox will return a URL to a path, which you would use with startAccessingSecurityScopedResource to access a specific file underneath that directory.
So I currently have this bit of code to get a dir:
-(NSString *)get {
NSOpenPanel *gitDir = [NSOpenPanel openPanel];
NSInteger *ger = [gitDir runModalForTypes:nil];
NSString *Directory = [gitDir directory];
return Directory;
}
But it gives me errors and says it has now been depreciated.
Is there a better way for OSX 10.7?
This is a supplement to sosborn's answer, not a replacement.
runModalForTypes: is deprecated, and the correct replacement is runModal (or setAllowedFileTypes: followed by runModal, but in this case you're passing nil for the types).
directory is also deprecated, and the correct replacement is directoryURL. (If you actually must return an NSString path rather than an NSURL, just return [[gitDir directoryURL] path].)
However, what you're doing is asking the user to select a file, and then returning the directory that file is in, when what you really want is to ask the user to select a directory. To do that, you want to call setCanChooseFiles to NO and setCanChooseDirectories to YES, and then call URLs to get the directory the user selected.
Also, you're ignoring the result of runModal (or runModalForTypes:). I'm sure the compiler is warning you about the unused variable "ger", and you shouldn't just ignore warnings. If the user cancels the panel, you're going to treat that as clicking OK, and select whatever directory she happened to be in when she canceled.
Here's a better implementation, which will return the URL of the selected directory, or nil if the user canceled (or somehow managed to not select anything). Again, if you need an NSString, just add a "path" call to the return statement:
-(NSURL *)get {
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setAllowsMultipleSelection:NO];
[panel setCanChooseDirectories:YES];
[panel setCanChooseFiles:NO];
if ([panel runModal] != NSModalResponseOK) return nil;
return [[panel URLs] lastObject];
}
Whenever you see a deprecation warning you should go straight to the official documentation. In this case, the docs for NSOpenPanel say:
runModalForTypes: Displays the panel and begins a modal event loop
that is terminated when the user clicks either OK or Cancel.
(Deprecated in Mac OS X v10.6. Use runModal instead. You can set
fileTypes using setAllowedFileTypes:.)
I adapted the code by abarnert for swift. tx for the code just what I needed.
func askUserForDirectory() -> NSURL? {
let myPanel:NSOpenPanel = NSOpenPanel()
myPanel.allowsMultipleSelection = false
myPanel.canChooseDirectories = true
myPanel.canChooseFiles = false
if ( myPanel.runModal() != NSFileHandlingPanelOKButton ) {
return nil
}
return myPanel.URLs[0] as? NSURL
}
I'm having a problem saving a string file with NSSavePanel after sandboxing the app for the Mac App Store. I set com.apple.security.files.user-selected.read-write to YES and the NSOpenPanel is working as it should.
When I try to save a new file, though, it seems that everything is working fine but then there is no saved file where it should be....
This is the code I am using to save the file:
NSSavePanel *save = [NSSavePanel savePanel];
long int result = [save runModal];
if (result == NSOKButton)
{
NSString *selectedFile = [save filename];
NSString *fileName = [[NSString alloc] initWithFormat:#"%#.dat", selectedFile];
NSString *arrayCompleto = [[NSString alloc]initWithFormat:#"bla bla bla"];
[arrayCompleto writeToFile:fileName
atomically:NO
encoding:NSUTF8StringEncoding
error:nil];
}
First of all, the -[NSSavePanel filename] selector has been deprecated. Use -[NSSavePanel URL] instead. Second, the way that the -[NSString writeToFile:atomically:encoding:error] tells you what you're doing wrong is with the error:(NSError**) argument.
You should also handle errors for file I/O in particular, because even if your code is 100% correct, there still might be errors on the user's system (insufficient privileges, etc.) and presenting the error to the user will allow them to see it failed (and have some idea why). Handling the error in code will also allow your app to recover. For instance, if you tried to read in the file below the code you pasted (after writing it to disk), but the user tried writing it to a network share they didn't have access to, your app might crash. If you know the write failed, you can proceed accordingly (perhaps prompting for a different save location).
In this case, though, I believe the following line is your problem:
NSString *fileName = [[NSString alloc] initWithFormat:#"%#.dat", selectedFile];
When your app is sandboxed, the user needs to give you permission for either a specific file or a specific directory through the open/save panels to bring them into your sandbox. What you're doing is taking the file the user gave you permission to write and saying "that's great, but I want to save a different file", which violates the sandbox. What you should do instead is set the extension in the Save Panel. The complete fixed solution would be:
NSSavePanel *save = [NSSavePanel savePanel];
[save setAllowedFileTypes:[NSArray arrayWithObject:#"dat"]];
[save setAllowsOtherFileTypes:NO];
NSInteger result = [save runModal];
if (result == NSOKButton)
{
NSString *selectedFile = [[save URL] path];
NSString *arrayCompleto = #"bla bla bla";
NSError *error = nil;
[arrayCompleto writeToFile:selectedFile
atomically:NO
encoding:NSUTF8StringEncoding
error:&error];
}
if (error) {
// This is one way to handle the error, as an example
[NSApp presentError:error];
}
If in the future something else is wrong, you can check the value of error at runtime. While debugging, set a breakpoint inside the if (error) statement to check error object's value (do a po error in Xcode's debugger). That should help you figure out what's wrong.