Drag file from NSTableView to other osx application - objective-c

I want to drag a file from a NSTableView row to copy it to another application (i.e. Finder). I implemented the first two steps ('Configuring Your Table View', 'Beginning a Drag Operation') of this guide and thought that would do the trick:
http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/TableView/Tasks/UsingDragAndDrop.html
However, when I attempt to drag a row, the row text follows my mouse but the file does not copy when released. Here's what I'm sending to my UITableView upon initialization:
#define librarySongDataType #"NSFileContentsPboardType"
- (void)awakeFromNib
{
[self setDraggingSourceOperationMask:NSUIntegerMax forLocal:YES]; // allow interapplication drags
[self registerForDraggedTypes:[NSArray arrayWithObject:librarySongDataType] ]; // NSFileContentsPboardType
}
Here's how I'm handling the drag in my NSTableView's data source (an NSArrayController):
- (BOOL)tableView:(NSTableView *)aTableView writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pboard
{
NSLog(#"writeRowsWithIndexes");
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes];
[pboard setData:data forType:librarySongDataType];
return YES;
}
To be clear, I'm not trying to drag files into my table view, I'm just trying to drag file(s) out of it.

Firstly, which document did you refer to when you wrote this line ?
[self setDraggingSourceOperationMask:NSUIntegerMax forLocal:YES];
This doesn't make sense. Don't use NSUIntegerMax. Use operation masks as defined here. It's written there that NSUIntegerMax stands for everything, but you shouldn't use it; Apple may re-define the bit in the future. You should use NSDragOperationCopy or something specific. If you copied that line from a webpage or a book, you should stop trusting that book/webpage.
Secondly, forLocal: should be NO to pass the data to another application; local here means application local.
Third, instead of just setting the archived data in
[pboard setData:data forType:librarySongDataType];
Consider making an NSFileWrapper and set it using writeFileWrapper:, see here. That way you can specify the file name to be created in Finder. Otherwise, the system doesn't have any idea what the data represent.

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.

NSOutlineView drag promises with directories

How does one implement an NSOutlineViewDataSource to allow dragging directories that do not exist in the file system at the time of dragging to the Finder? I have searched and searched and read a ton of the documentation, but have had great difficulty finding anything of value that works. I use a custom data source that manages a file system-like tree, and all the items are instances of a class that keeps track of its path. I would like to be able to drag files and directories out of the outline view into the Finder.
I have:
- (BOOL)outlineView:(NSOutlineView *)outlineView
writeItems:(NSArray *)items
toPasteboard:(NSPasteboard *)pasteboard
{
NSMutableArray *types = [NSMutableArray array];
for (JOItemInfo *itemInfo in items) {
NSString *extension = itemInfo.name.pathExtension;
if (extension.length > 0) [types addObject:extension];
}
[pasteboard declareTypes:#[(__bridge_transfer NSString *)kPasteboardTypeFileURLPromise]
owner:self];
[pasteboard setPropertyList:types
forType:(__bridge_transfer NSString *)kPasteboardTypeFileURLPromise];
DDLogInfo(#"Wrote types %# to pasteboard %# for key %#",
types,
pasteboard,
(__bridge_transfer NSString *)kPasteboardTypeFileURLPromise);
return YES;
}
and an implementation of -outlineView:namesOfPromisedFilesDroppedAtDestination:forDraggedItems: that writes the items inside of the given path. This works in that I can drag items out to the Finder, but when I let go nothing else happens, and the -...namesOfPromisedFilesDropped... method isn't even called. Also,
[self.outlineView setDraggingDestinationFeedbackStyle:NSTableViewDraggingDestinationFeedbackStyleRegular];
[self.outlineView setDraggingSourceOperationMask:NSDragOperationNone forLocal:YES];
[self.outlineView setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
is in my -awakeFromNib. The if (extension.length > 0) ... was based on an example I found somewhere, but it was dated, and the documentation says to return an extension, so I think that is appropriate. Personally, I find the documentation for this whole area very lacking, especially in regard to an NSOutlineView. Thanks!
I changed (__bridge_transfer NSString *)kPasteboardTypeFileURLPromise to NSFilesPromisePboardType, and I can now drag files (with an extension at least) and they can be dropped successfully in the Finder. (I had used the former b/c the documentation for the latter recommended that, but they do not have the same effect.)
Also, I tried removing the conditional and allowing it to add an empty string for an empty extension, which worked like a charm. I can now drag out of the outline view into the Finder.

Document based OSX app - Limit number of open documents to one

I'm trying to figure out how to limit my NSDocument based application to one open document at a time. It is quickly becoming a mess.
Has anyone been able to do this in a straightforward & reliable way?
////EDIT////
I would like to be able to prompt the user to save an existing open document and close it before creating/opening a new document.
////EDIT 2
I'm now trying to just return an error with an appropriate message if any documents are opening -- however, the error message is not displaying my NSLocalizedKeyDescription. This is in my NSDocumentController subclass.
-(id)openUntitledDocumentAndDisplay:(BOOL)displayDocument error:(NSError **)outError{
if([self.documents count]){
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithObject:#"Only one document can be open at a time. Please close your document." forKey:NSLocalizedDescriptionKey];
*outError = [NSError errorWithDomain:#"Error" code:192 userInfo:dict];
return nil;
}
return [super openUntitledDocumentAndDisplay:displayDocument error:outError];
}
It won't be an easy solution, since it's a pretty complex class, but I would suggest that you subclass NSDocumentController and register your own which disables opening beyond a certain number of documents. This will allow you to prevent things like opening files by dropping them on the application's icon in the dock or opening in the finder, both of which bypass the Open menu item.
You will still need to override the GUI/menu activation code to prevent Open... from being available when you have a document open already, but that's just to make sure you don't confuse the user.
Your document controller needs to be created before any other document controllers, but that's easy to do by placing a DocumentController instance in your MainMenu.xib and making sure the class is set to your subclass. (This will cause it to call -sharedDocumentController, which will create an instance of yours.)
In your document controller, then, you will need to override:
- makeDocumentForURL:withContentsOfURL:ofType:error:
- makeUntitledDocumentOfType:error:
- makeDocumentWithContentsOfURL:ofType:error:
to check and see if a document is already open and return nil, setting the error pointer to a newly created error that shows an appropriate message (NSLocalizedDescriptionKey).
That should take care of cases of drag-and-drop, applescript,etc.
EDIT
As for your additional request of the close/save prompt on an opening event, that's a nastier problem. You could:
Save off the information (basically the arguments for the make requests)
Send the -closeAllDocumentsWithDelegate:didCloseAllSelector:contextInfo: with self as a delegate and a newly-created routine as the selector
When you receive the selector, then either clear out the saved arguments, or re-execute the commands with the arguments you saved.
Note that step 2 and 3 might need to be done on delay with performSelector
I haven't tried this myself (the rest I've done before), but it seems like it should work.
Here's the solution I ended up with. All of this is in a NSDocumentController subclass.
- (NSInteger)runModalOpenPanel:(NSOpenPanel *)openPanel forTypes:(NSArray *)extensions{
[openPanel setAllowsMultipleSelection:NO];
return [super runModalOpenPanel:openPanel forTypes:extensions];
}
-(NSUInteger)maximumRecentDocumentCount{
return 0;
}
-(void)newDocument:(id)sender{
if ([self.documents count]) {
[super closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(newDocument:didCloseAll:contextInfo:) contextInfo:(void*)sender];
}
else{
[super newDocument:sender];
}
}
- (void)newDocument:(NSDocumentController *)docController didCloseAll: (BOOL)didCloseAll contextInfo:(void *)contextInfo{
if([self.documents count])return;
else [super newDocument:(__bridge id)contextInfo];
}
-(void)openDocument:(id)sender{
if ([self.documents count]) {
[super closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(openDocument:didCloseAll:contextInfo:) contextInfo:(void*)sender];
}
else{
[super openDocument:sender];
}
}
- (void)openDocument:(NSDocumentController *)docController didCloseAll: (BOOL)didCloseAll contextInfo:(void *)contextInfo{
if([self.documents count])return;
else [super openDocument:(__bridge id)contextInfo];
}
Also, I unfortunately needed to remove the "Open Recent" option from the Main Menu. I haven't figured out how to get around that situation.

Need a view that acts like a log view

I need to implement a view that acts as a log view, so that when you push a message into it, the message would push other messages upwards.
Is there anything like that for iOS?
You can easily implement that using standard UITableView:
Each cell will be responsible for displaying 1 log message
Add new cell to the end of the table when new message arrive
Scroll table to the bottom after cell is added (using scrollToRowAtIndexPath:atScrollPosition:animated: method with UITableViewScrollPositionBottom position parameter)
That means you'll need to store your log messages in array, but if you're going to display them you need to store messages anyway
#Vladimir's answer is probably the way to go, but just for the sake of seeing some additional options, here's an example using a UITextView:
- (IBAction)addNewLog:(UIButton *)sender {
NSString *myInputText = #"some new text from string";
NSString *temp = myTextView.text;
[myTextView setText:[temp stringByAppendingString:[NSString stringWithFormat:#"\n%#: %#",[NSDate date],myInputText]]];
[myTextView setContentOffset:CGPointMake(0, myTextView.contentSize.height - myTextView.frame.size.height) animated:NO];
}
Then if you wanted to separate the text in the text view into objects in an array:
NSArray *myAwesomeArray = [myTextView.text componentsSeparatedByString:#"\n"];
Mind you, the above would break if the "myInputText" string ever contained a line break.

Navigating between View controllers?

In my Iphone application I am trying to navigate from one table view controller to next table view controller. Problem I am facing is that I have to fetch data using http request and then parse this data when the user select a cell. I am able to fetch and parse the data but the view controller is not waiting for the data to parsed and the next view controller is shown (which is empty). How to over come this problem.
indexSelected = [NSString stringWithFormat: #"%d",[indexPath row] ];
[[MySingletonClass sharedMySingleton] doAnAuthenticatedAPIFetch_Subscriber_Detail : indexSelected];
SubscribersDetailViews2 *viewController = [[SubscribersDetailViews2 alloc] initWithNibName:#"SubscribersDetailViews2" bundle:nil];
[[self navigationController] pushViewController:viewController animated:YES];
[viewController release];
This is what you do:
indexSelected = [NSString stringWithFormat: #"%d",[indexPath row] ];
SubscribersDetailViews2 *viewController = [[SubscribersDetailViews2 alloc] initWithNibName:#"SubscribersDetailViews2" bundle:nil];
[[MySingletonClass sharedMySingleton] doAnAuthenticatedAPIFetch_Subscriber_Detail:indexSelected delegate:self];
[[self navigationController] pushViewController:viewController animated:YES];
[viewController release];
You define a protocol that your view controller conforms to and when the fetching and parsing of data is done you call a method on the delegate to let the view controller know that the data is ready to be displayed.
If you need more information on how to do this, leave a comment.
EDIT: So here's how to declare and use a protocol. I'm going to try to keep it as simple as possible. I'm not sure if I like your naming convention, but I'll still use it for this example.
So let's get down to the code. This is how you declare a protocol:
#protocol MySingletonClassDelegate <NSObject>
#optional
- (void)didDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithData:(NSArray *)data;
- (void)failedToDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithError:(NSError *)error;
#end
Again, I'm not too fond of the naming convention. You shouldn't have underscores in objective-c method names.
The protocol should be defined in MySingletonClass.h before the declaration of MySingletonClass.
I declared two methods in the protocol, one for delivering the data and one for delivering an error if it fails, so that you can notify the user that it failed.
To use the protocol you need the following:
#interface SubscribersDetailViews2 : UITableViewController <MySingletonClassDelegate>
You also need to implement the methods declared in the protocol, but I'll leave that implementation to you.
Since the fetching of data already seems to be happening in the background I don't think I'll need to explain how to do that. One important thing to remember is that you want to execute the delegate methods on the main thread. Here's the code to do that:
- (void)doAnAuthenticatedAPIFetch_Subscriber_Detail:(NSUInteger)index delegate:id<MySingletonClassDelegate>delegate {
// Fetching data in background
if (successful) {
[self performSelectorOnMainThread:#selector(didDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithData:) withObject:data waitUntilDone:NO];
} else {
[self performSelectorOnMainThread:#selector(failedToDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithError:) withObject:error waitUntilDone:NO];
}
}
Just to be clear the // Fetching data in background is supposed to be replaced by your code. I assume that your code produces the variables (NSArray *data, NSError *error, BOOL successful) that I use.
That's about it, if you need clarification on anything let me know.
There are a number of options:
Cache the data, i.e., take a full copy of it on the iOS device (may not be practical of course)
Display an interstitial screen saying "loading" and then move to the "real" screen when the data has downloaded
Have, effectively, two different data sources for your table. The first is your current one. The second would be a single cell saying "Loading..."
In short, there's no point and click way of doing this but there's no problem downloading the data on the fly as long as you tell your users what's happening.