NSOpenPanel allowed to select packages but not show contents - objective-c

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!

Related

How to use NSPasteboard with kPasteboardTypeFileURLPromise for copy/paste?

My application would like to add a promise to the pasteboard for a file that is stored remotely, and may never be pasted—similar to pasting a file copied from a session controlling a VM or other remote system. Ideally, a user can paste in a Finder folder (or the desktop) and the promise would trigger and away we go. I am willing to deal with the issues of fulfilling the promise once triggered, but I have been unable to get the promise to trigger.
All of the promise code I have found deals with drag and drop, which is not functionality what I need (though it is possible that something from DnD needs to be in place for promises to work?)
I have tried using NSFilePromiseProvider with a delegate, and adding that to the pasteboard. I can see the entries on the pasteboard using a clipboard viewer, but when I paste in Finder nothing happens and no delegate methods are called. I can trigger the delegate methods by having the clipboard viewer access the entries, so I know that much is hooked up.
#interface ClipboardMacPromise : NSFilePromiseProvider<NSFilePromiseProviderDelegate>
{
NSString* m_file;
}
#end
#implementation ClipboardMacPromise
- (id)initWithFileType:(NSString*)type andFile:(NSString*)file
{
m_file = file;
return [super initWithFileType:type delegate:self];
}
- (NSString *)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider fileNameForType:(NSString *)fileType
{
return m_file;
}
- (void)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider writePromiseToURL:(NSURL *)url completionHandler:(void (^)(NSError * _Nullable errorOrNil))completionHandler
{
// Finder can't paste, so we never get here...
}
#end
NSPasteboard* pboard = [NSPasteboard generalPasteboard];
[pboard clearContents];
NSMutableArray* items = [[NSMutableArray alloc] init];
ClipboardMacPromise* promise = [[ClipboardMacPromise alloc] initWithFileType:(NSString*)kUTTypeFileURL andFile:#"dummy.txt"];
[items addObject:promise];
[pboard writeObjects:items];
I have also tried NSPasteboardItem with NSPasteboardItemDataProvider where I setup a promise for content on kUTITypeFileURL. It provided very similar entries on the pasteboard, but still no action when I paste in finder. Clipboard viewer will again trigger the provider fine when accessing the individual pasteboard entries. (NSPasteboard's declareTypes:owner: has the same behavior)
#interface ClipboardMacPromise : NSPasteboardItem<NSPasteboardItemDataProvider>
{
NSString* m_file;
}
#end
#implementation ClipboardMacPromise
- (id)initWithFile:(NSString*)file
{
m_file = file;
id _self = [super init];
if (_self) {
[_self setDataProvider:_self forTypes:#[(NSString*)kPasteboardTypeFileURLPromise]];
[_self setString:(NSString*)kUTTypeFileURL forType:(NSString*)kPasteboardTypeFilePromiseContent];
}
return _self;
}
- (void)pasteboard:(NSPasteboard *)pasteboard item:(NSPasteboardItem *)item provideDataForType:(NSPasteboardType)type
{
// we don't get here when we paste in Finder because
// Finder doesn't think there's anything to paste
// but using a clipboard viewer, we can force the promise to
// resolve and we do get here
}
#end
NSPasteboard* pboard = [NSPasteboard generalPasteboard];
[pboard clearContents];
NSMutableArray* items = [[NSMutableArray alloc] init];
ClipboardMacPromise* promise = [[ClipboardMacPromise alloc] initWithFile:#"file:///tmp/dummy.txt"];
[items addObject:promise];
[pboard writeObjects:items];
And for completeness, here is my Carbon attempt since Pasteboard.h seems to detail how this should work in a copy/paste scenario... but it still does not provide Finder what it is looking for. The generated clipboard entries look very similar between the three implementations.
OSStatus PasteboardPromiseKeeperProc(PasteboardRef pasteboard, PasteboardItemID item, CFStringRef flavorType, void * _Nullable context)
{
// 6) The sender's promise callback for kPasteboardTypeFileURLPromise is called.
string s = "dummy.txt";
CFDataRef inData = CFDataCreate(kCFAllocatorDefault, (UInt8*)s.c_str(), s.size());
PasteboardPutItemFlavor(pasteboard, item, flavorType, inData, 0);
return noErr;
}
PasteboardRef p = NULL;
PasteboardCreate(kPasteboardClipboard, &p);
PasteboardClear(p);
PasteboardSetPromiseKeeper(p, &PasteboardPromiseKeeperProc, this);
// 1) The sender promises kPasteboardTypeFileURLPromise for a file yet to be created.
PasteboardPutItemFlavor(p, (PasteboardItemID)1, kPasteboardTypeFileURLPromise, kPasteboardPromisedData, 0);
// 2) The sender adds kPasteboardTypeFilePromiseContent containing the UTI describing the file's content.
PasteboardPutItemFlavor(p, (PasteboardItemID)2, kPasteboardTypeFilePromiseContent,CFStringCreateExternalRepresentation(NULL, kUTTypeFileURL, kCFStringEncodingUTF8, 0), 0);
It really seems that there is a certain UTI that Finder is looking for on the pasteboard, and I don't have it. If I put a kUTTypeFileURL directly on the clipboard, it appears that finder actually checks for the existence of the file (ie. triggers Catalina's Desktop access prompt) before offering it to paste.
Does anyone know if or how file promises can be provided to Finder through Copy/Paste instead of Drag-and-Drop?
It appears that the key piece here is that Finder requires that the file actually be present on disk for the paste action to be enabled for a file URL. This one detail rules out the possibility of promises working for copy/paste -- at least with Finder.
The correct solution therefore requires a virtualized file system (like FUSE) so that the promises can be made and fulfilled at the filesystem level. Thus a collection of temporary zero-length files can be written to disk, and actual file URLs be added to the pasteboard. This fulfills the requirements that Finder has to enable paste. Then when a paste action is made, the file data is read from the virtualized file system which can in turn retrieve the actual data from the remote system. Finder is none the wiser. The copy will even have a built in progress bar!
It appears that Microsoft's Mac RDP client mostly works this way, although I was only ever able to get it to copy zero length files so this may be harder to get right than it sounds.

Setting up NSDocument for saving a non-native type

My application has a custom format for which it is registered in Info.plist as being an editor and can read and write to that format. It can also read a number of other formats, but cannot write to them. For these formats it is registered in Info.plist as being a viewer.
When I open one of the other formats, everything seems fine, but when I come to save the file my NSDocument is sent the message writeToURL:ofType:error: with the URL of the file I loaded and the type as the UTI of the format I cannot write.
-(NSArray *)writableTypes only returns my custom format's UTI, and -(BOOL)isNativeType: only returns YES for my custom format's UTI.
What I'd like to do is, like other applications that have a native format but can read from other formats, when the user presses Save, the save panel is opened and the user selects a filename in which to save as the native type.
Is this something that NSDocument can do itself, or do I need to check in writeToURL:ofType:error: to see if I need to open the save panel manually?
Answering myself, the solution I found that appears to work is in readFromURL:ofType:error: to reset the NSDocument's fileURL, displayName and fileType for the non-native formats.
- (BOOL)readFromURL:(NSURL *)url ofType:(NSString *)type error:(NSError **)error
{
// Handle loading as normal
if (![type isEqualToString:kMyNativeType]) {
[self setFileType:kMyNativeType];
[self setFileURL:nil];
// Without this, all non-native files are displayed as Untitled.
// This sets the window title as the filename without the extension
NSArray *filenameComponents = [[url lastPathComponent] componentsSeparatedByString:#"."];
[self setDisplayName:filenameComponents[0]];
}
return YES;
}
I don't know if this is the correct way, but it appears to work for what I want at the moment, and appears to be what is suggested in https://developer.apple.com/library/mac/documentation/DataManagement/Conceptual/DocBasedAppProgrammingGuideForOSX/AdvancedTopics/AdvancedTopics.html#//apple_ref/doc/uid/TP40011179-CH7-SW8

Drag Files come across Sandbox(__CFPasteboardIssueSandboxExtensionForPath)

I processed drag operation from browser view to custom view.It work well in snow lepoard,but not in Mountain Lion with sandbox.
in browser view:
NSMutableArray* urls = [[[NSMutableArray alloc] init] autorelease];
..............put some NSUrl to urls array....................
[pasteboard writeObjects:[NSArray arrayWithArray:urls]];
in my receive custom view:
NSArray* pasteboardItems = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSString class]] options:nil];
NSArray* pasteboardItems2 = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:nil];
NSArray* pasteboardItems3 = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSImage class]] options:nil];
NSLog(#"%#",pasteboardItems);
NSLog(#"%#",pasteboardItems2);
NSLog(#"%#",pasteboardItems3);
my log is:
2012-08-09 18:33:43.886 iCollage[6885:303] __CFPasteboardIssueSandboxExtensionForPath: error for [/Users/xxxx/Library/Containers/xxxxxxxxxxxx/Data/Downloads/1343902069.jpg]
2012-08-09 18:33:44.546 iCollage[6885:303] ( "file://localhost/Users/xxx/Library/Containers/xxxxxxxx/Data/Downloads/1343902069.jpg")
2012-08-09 18:33:44.547 iCollage[6885:303] ( "file://localhost/Users/xxxxx/Library/Containers/xxxxxx/Data/Downloads/1343902069.jpg")
2012-08-09 18:33:44.547 iCollage[6885:303] ()
my question is:
1.how to fix this error __CFPasteboardIssueSandboxExtensionForPath;I refer the docs and found nothing about that.I am ensuer that i have the permission to access the file!google says, may be "startAccessingSecurityScopedResource" will help me, then i try and failed
2.why pasteboardItems2 have value?i write to pasteboard only url but not string.It disgusted me that I can get the url both from NSString type and NSUrl type! (I try drag a file from iFinder, the url will only exist in pasteboardItems but not pasteboardItems2).Anybody know why? I think the first problem will auto fixed when some one help me fix this problem.
I believe Apple answer question 1:
Important: Although you can support dragging file paths, in general,
you should avoid doing so unless you are certain that the destination
app will never be run in an app sandbox. If you use an NSString, OS X
has no way to know whether that string should be interpreted as a
path; thus, OS X does not expand the destination app’s sandbox to
allow access to the file or directory at that location. Instead, use
an NSURL, a bookmark, or a filename pasteboard type.
WRT to question 2, it looks like you have pasted URLs so reading NSURL objects would seem to be correct. However I think you should implement the dragging using the following code (also from the link above):
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
NSPasteboard *pboard = [sender draggingPasteboard];
if ( [[pboard types] containsObject:NSFilenamesPboardType] ) {
NSArray *files = [pboard propertyListForType:NSFilenamesPboardType];
int numberOfFiles = [files count];
// Perform operation using the list of files
}
return YES;
}
You need to generate security-scoped URL bookmark data on the sender side, and turn that data back into a URL on the receiver side. There's some other stuff you have to do after that when you want to actually access the URL; the documentation elaborates.
The receiving application, when running in a sandbox, will not be able to handle bare paths. This is a core part of being sandboxed; you are not allowed to use bare paths or their corresponding URLs to access files that aren't in your sandbox container and haven't been explicitly handed to you by the user.
Your pasteboardItems read object of NSString type, but you dragged a file(with jpg extension), you should register for NSString type in your init method:
[self registerForDraggedTypes:[NSArray arrayWithObject:NSPasteboardTypeString]];
You need to have Document Types defined in your application so that the sandboxing mechanism knows your application should be opening files with those extensions. You can do this by clicking the project on the left in Xcode, and in the Info tab, under Document Types add a new document type for each extension.
You just need to fill in the name and extensions field.
Also if you want to persist your permission to access the files dragged onto your application, you can use this class to wrap up all that logic. https://github.com/leighmcculloch/AppSandboxFileAccess

Objective-C Directory Picker ( OSX 10.7 )

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
}

Prevent warning when NSDocument file is programmatically renamed

My application allows the user to rename documents that are currently open. This is trivial, and works fine, with one really annoying bug I can't figure out. When a file is renamed, AppKit (kindly) warns the user the next time they try to save the document. The user says "OK" and everything continues as normal. This makes sense when something external to the application changed the document, but not when it was actually done by the document itself.
The code goes something like this:
-(void)renameDocumentTo:(NSString *)newName {
NSURL *newURL = [[[self fileURL] URLByDeletingLastPathComponent]
URLByAppendingPathComponent:newName];
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager moveItemAtURL:[self fileURL] toURL:newURL];
NSDictionary *attrs = [fileManager attributesForItemAtPath:[newURL path] error:NULL];
[self setFileURL:newURL];
[self setFileModificationDate:[attrs fileModificationDate]];
}
One would think that expressly setting the new URL and modification date on the document would be enough, but sadly it's not. Cocoa still generates the warning.
I've tried changing the order (setting the new URL on the document, THEN renaming the file) but this doesn't help.
I've also tried a fix suggested by a user on an old post over at CocoaDev:
[self performSelector:#selector(_resetMoveAndRenameSensing)];
Even this does not stop the warning however, and I'm guessing there has to be a proper way to do this using the documented API. How does Xcode handle things when a user clicks a file on the project tree and renames it to something else. It doesn't warn the user about the rename, since the user actually performed the rename.
What do I need to do?
There isn't much on this in the main docs. Instead, have a look at the 10.5 release notes: http://developer.apple.com/library/mac/#releasenotes/Cocoa/AppKitOlderNotes.html%23X10_5Notes under the heading "NSDocument Checking for Modified Files At Saving Time"
(In the case of Xcode, it has a long history and I wouldn't be surprised if if doesn't use NSDocument for files within the project)
It is worth noting that moving a file does not change its modification date, so calling -setFileModificationDate: is unlikely to have any effect.
So one possibility could be to bypass NSDocument's usual warning like so:
- (void)saveDocument:(id)sender;
{
if (wasRenamed)
{
[self saveToURL:[self fileURL] ofType:[self fileType] forSaveOperation:NSSaveOperation delegate:nil didSaveSelector:nil contextInfo:NULL];
wasRenamed = NO;
}
else
{
[super saveDocument:sender];
}
}
Ideally you also need to check for the possibility of:
Ask app to rename the doc
Renamed file is then modified/moved by another app
User goes to save the doc
At that point you want the usual warning sheet to come up. Could probably be accomplished by something like:
- (void)renameDocumentTo:(NSString *)newName
{
// Do the rename
[self setFileURL:newURL];
wasRenamed = YES; // MUST happen after -setFileURL:
}
- (void)setFileURL:(NSURL *)absoluteURL;
{
if (![absoluteURL isEqual:[self fileURL]]) wasRenamed = NO;
[super setFileURL:absoluteURL];
}
- (void)setFileModificationDate:(NSDate *)modificationDate;
{
if (![modificationDate isEqualToDate:[self fileModificationDate]]) wasRenamed = NO;
[super setFileModificationDate:modificationDate];
}
Otherwise, your only other choice I can see is to call one of the standard save/write methods with some custom parameters that prompt your document subclass to move the current doc rather than actually save it. Would be trickier I think. Perhaps define your own NSSaveOperationType?
With this technique the doc system should understand that the rename was part of a save-like operation, but it would need quite a bit of experimentation to be sure.
Much inspired from #Mike's answer, I got the "moved to" message not to show up anymore by re-routing NSSaveOperation to NSSaveAsOperation. In my NSDocument subclass:
I overload saveDocumentWithDelegate:didSaveSelector:contextInfo: to determine the save URL and document type (assigning those to self); if the old fileURL exists, I move that to the new location
Inside saveDocumentWithDelegate:didSaveSelector:contextInfo: I redirect the call to [self saveToURL:self.fileURL ofType:self.fileType forSaveOperation:NSSaveAsOperation completionHandler: ...] instead of [super saveDocumentWithDelegate:didSaveSelector:contextInfo:]
This works for me.
Isn't it possible to programmatically answer the question for the user?
Or you can save immediately after renaming, this way a user gets every answer in one go.
I see that this problem is up and running for some time, so telling you to read the reference won't do any good i guess..
Hope i helped a little bit although it doesn't fix your problem directly