Custom NSView Drag Destination - macos

I'm trying to create a simple NSView that will allow a folder from Finder to be dragged onto it. A folder path is the only thing I want the view to accept as a draggable item. I've been trying to follow the Apple documentation, but so far nothing's working. So far, I've just tried to get the view to work with any file type, but I can't even seem to do that. Here's what I have so far:
-(id) initWithFrame:(NSRect)frameRect
{
if (self = [super initWithFrame:frameRect])
{
NSLog(#"getting called");
[self registerForDraggedTypes:[NSArray arrayWithObjects:NSPasteboardTypeString,
NSPasteboardTypePDF,
NSPasteboardTypeTIFF,
NSPasteboardTypePNG,
NSPasteboardTypeRTF,
NSPasteboardTypeRTFD,
NSPasteboardTypeHTML,
NSPasteboardTypeTabularText,
NSPasteboardTypeFont,
NSPasteboardTypeRuler,
NSPasteboardTypeColor,
NSPasteboardTypeSound,
NSPasteboardTypeMultipleTextSelection,
NSPasteboardTypeFindPanelSearchOptions, nil]];
}
return self;
}
-(BOOL) prepareForDragOperation: (id<NSDraggingInfo>) sender
{
NSLog(#"preparing for drag");
return YES;
}
The initWithFrame: method is getting called, but when I try to drag into the view the prepareForDragOperation: method doesn't ever seem to get called. My questions:
What am I doing wrong? Why isn't prepareForDragOperation: ever getting called?
What do I need to do to get the drag operation to only support dragging folders?
Update
I updated my registerForDraggedTypes: method with every type I could find. It now looks like this:
[self registerForDraggedTypes:[NSArray arrayWithObjects:NSPasteboardTypeString,
NSPasteboardTypePDF,
NSPasteboardTypeTIFF,
NSPasteboardTypePNG,
NSPasteboardTypeRTF,
NSPasteboardTypeRTFD,
NSPasteboardTypeHTML,
NSPasteboardTypeTabularText,
NSPasteboardTypeFont,
NSPasteboardTypeRuler,
NSPasteboardTypeColor,
NSPasteboardTypeSound,
NSPasteboardTypeMultipleTextSelection,
NSPasteboardTypeFindPanelSearchOptions,
NSStringPboardType,
NSFilenamesPboardType,
NSPostScriptPboardType,
NSTIFFPboardType,
NSRTFPboardType,
NSTabularTextPboardType,
NSFontPboardType,
NSRulerPboardType,
NSFileContentsPboardType,
NSColorPboardType,
NSRTFDPboardType,
NSHTMLPboardType,
NSURLPboardType,
NSPDFPboardType,
NSVCardPboardType,
NSFilesPromisePboardType,
NSMultipleTextSelectionPboardType, nil]];
I've noticed that the prepareForDragOperation: method isn't getting called when I drag a folder into the view. Did I miss a step?

Here's a simple little drag & drop view meeting those criteria:
MDDragDropView.h:
#interface MDDragDropView : NSView {
BOOL isHighlighted;
}
#property (assign, setter=setHighlighted:) BOOL isHighlighted;
#end
MDDragDropView.m:
#implementation MDDragDropView
#dynamic isHighlighted;
- (void)awakeFromNib {
NSLog(#"[%# %#]", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
[self registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, nil]];
}
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender {
NSLog(#"[%# %#]", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
NSPasteboard *pboard = [sender draggingPasteboard];
if ([[pboard types] containsObject:NSFilenamesPboardType]) {
NSArray *paths = [pboard propertyListForType:NSFilenamesPboardType];
for (NSString *path in paths) {
NSError *error = nil;
NSString *utiType = [[NSWorkspace sharedWorkspace]
typeOfFile:path error:&error];
if (![[NSWorkspace sharedWorkspace]
type:utiType conformsToType:(id)kUTTypeFolder]) {
[self setHighlighted:NO];
return NSDragOperationNone;
}
}
}
[self setHighlighted:YES];
return NSDragOperationEvery;
}
And the rest of the methods:
- (void)draggingExited:(id <NSDraggingInfo>)sender {
[self setHighlighted:NO];
}
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender {
return YES;
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
[self setHighlighted:NO];
return YES;
}
- (BOOL)isHighlighted {
return isHighlighted;
}
- (void)setHighlighted:(BOOL)value {
isHighlighted = value;
[self setNeedsDisplay:YES];
}
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
if (isHighlighted) {
[NSBezierPath setDefaultLineWidth:6.0];
[[NSColor keyboardFocusIndicatorColor] set];
[NSBezierPath strokeRect:self.frame];
}
}
#end
The reason prepareForDragOperation: isn't being called is that the dragging destination sequence follows a precise set of steps, and if the earlier steps aren't implemented, or are implemented but return a "stop the drag operation" type of answer, the later methods are never reached. (In your case, it doesn't appear that you've implemented the draggingEntered: method, which would need to return something other than NSDragOperationNone to continue on in the sequence).
Before prepareForDragOperation: is sent, the view is first sent a series of dragging destination messages:
A single - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender.
Depending on the NSDragOperation mask returned from that method, the following will be called if it's implemented in your class:
Multiple - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender.
Depending on the NSDragOperation mask returned from that method, then prepareForDragOperation: will be called.

I'm using NSURLPboardType to register for stuff being dropped from the Finder (when I drag a file or a folder to my application, it receives them as urls)
Try this. And if it works, it'll solve your second problem : just check if the URL is a folder to accept or reject the drop :
// if item is an NSURL * :
CFURLHasDirectoryPath((CFURLRef)item)
// returns true if item is the URL of a folder.

Related

Why do NSFilePresenter protocol methods never get called?

I am trying to monitor file changes in local and iCloud directories and have implemented the NSFilePresenter protocol methods but the only method that gets called is presentedItemAtURL.
Am I correct in assuming that I should be able to monitor a local or an iCloud directory and get notified any time any process adds, modifies or deletes a file in the directory.
Here is the basic code for the OS X App:
- (void)awakeFromNib {
_presentedItemURL = myDocumentsDirectoryURL;
_presentedItemOperationQueue = [[NSOperationQueue alloc] init];
[_presentedItemOperationQueue setMaxConcurrentOperationCount: 1];
_fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self];
}
- (NSURL*) presentedItemURL {
FLOG(#" called %#", _presentedItemURL);
return _presentedItemURL;
}
- (NSOperationQueue*) presentedItemOperationQueue {
FLOG(#" called");
return _presentedItemOperationQueue;
}
- (void)presentedItemDidChange {
FLOG(#" called");
dispatch_async(dispatch_get_main_queue(), ^{
[self reloadData];
});
}
-(void)accommodatePresentedItemDeletionWithCompletionHandler:(void (^)(NSError *errorOrNil))completionHandler
{ FLOG(#" called");
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self reloadData];
}];
completionHandler(nil);
}
-(void)presentedSubitemDidChangeAtURL:(NSURL *)url {
FLOG(#" called");
dispatch_async(dispatch_get_main_queue(), ^{
[self reloadData];
});
}
-(void)presentedSubitemDidAppearAtURL:(NSURL *)url {
FLOG(#" called");
dispatch_async(dispatch_get_main_queue(), ^{
[self reloadData];
});
}
Long time ago, I know, but perhaps this will still help. NSFilePresenter will only notify you about changes made by another process that makes changes to a directory or file USING AN NSFileCoordinator. If another process (eg: iTunes file sharing) makes changes without an NSFileCoordinator, you won't be notified.
This is in no way my final implementation and I will edit/update as I improve. But since there is nil examples on how to do this, i figured i'd share something that works!!! That's right, it works. I am able to read the file in my app, and at the same time make a change in textedit and the changes propagate to my app. Hope this helps bud.
PBDocument.h
#interface PBDocument : NSObject <NSFilePresenter>
#property (nonatomic, strong) NSTextView *textView;
#pragma mark - NSFilePresenter properties
#property (readonly) NSURL *presentedItemURL;
#property (readonly) NSOperationQueue *presentedItemOperationQueue;
- (instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError *__autoreleasing *)outError textView:(NSTextView*)textView;
#end
PBDocument.m
#interface PBDocument ()
#property (readwrite) NSURL *presentedItemURL;
#property (readwrite) NSOperationQueue *presentedItemOperationQueue;
#property (readwrite) NSFileCoordinator *fileCoordinator;
#end
#implementation PBDocument
- (instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError *__autoreleasing *)outError textView:(NSTextView*)textView {
self = [super init];
if (self) {
_textView = textView;
_presentedItemURL = url;
_presentedItemOperationQueue = [NSOperationQueue mainQueue];
[NSFileCoordinator addFilePresenter:self];
_fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self];
[self readWithCoordination];
}
return self;
}
- (void)readWithCoordination {
NSError *error = nil;
[self.fileCoordinator coordinateReadingItemAtURL:_presentedItemURL options:NSFileCoordinatorReadingWithoutChanges error:&error byAccessor:^(NSURL *newURL) {
NSLog(#"Coordinating Read");
NSError *error = nil;
NSFileWrapper *wrapper = [[NSFileWrapper alloc] initWithURL:newURL options:0 error:&error];
if (!error) {
[self readFromFileWrapper:wrapper ofType:[self.presentedItemURL pathExtension] error:&error];
}
if (error) #throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:#"%#", error] userInfo:nil];
}];
if (error) #throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:#"%#", error] userInfo:nil];
}
- (void)presentedItemDidChange {
[self readWithCoordination];
}
#end
If it's any help to anyone this is the approach (FSEvents) I ended up using recently for a file sync solution and it seems to work for any file system. I have not done any research recently on NSFileCoordinator to see whether this is better worse or what the use cases are as a comparison.
I also did not test every use case so your mileage may vary.
https://github.com/eonil/FSEvents

draggingEntered not called

I have an NSBox subclass called dragBox. I want to be able to drag it around a canvas. The code is as follows:
-(void) awakeFromNib
{
[[self superview] registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
}
-(void) mouseDown:(NSEvent *)theEvent
{
[self dragImage:[[NSImage alloc] initWithContentsOfFile:#"/Users/bruce/Desktop/Untitled-1.png"] at:NSMakePoint(32, 32) offset:NSMakeSize(0,0) event:theEvent pasteboard:[NSPasteboard pasteboardWithName:NSDragPboard] source:self slideBack:YES];
}
-(NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender // validate
{
NSLog(#"Updated");
return [sender draggingSourceOperationMask];
}
-(NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender {
NSLog(#"Drag Entered");
return [sender draggingSourceOperationMask];
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
NSLog(#"Move Box");
[self setFrameOrigin:[sender draggingLocation]];
return YES;
}
-(BOOL) prepareForDragOperation:(id<NSDraggingInfo>)sender
{NSLog(#"Prepared");
return YES;
}
Why isn't dragEntered being called? I have tried to use all the pboard types and such. Nothing seems to work. I have also changed the registerForDraggedTypes to just work off of the [self] view. The box is a subview of a canvas.
Bruce
I found that awakeFromNib was the wrong place to put my registerForDragTypes call since I am programmatically adding my view (i.e. not adding it via a Nib). I had to put the call into initWithFrame:
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self registerForDraggedTypes: [NSArray arrayWithObjects:NSTIFFPboardType,NSFilenamesPboardType,nil]];
}
return self;
}
Bruce,
Your Code needs to be changed in the below way. I believe that view should be registered for drag types to make the method draggingEntered to get called.
#interface NSModifiedBox : NSBox
#end
#implementation NSModifiedBox
- (void)drawRect:(NSRect)dirtyRect
{
// Drawing code here.
[self registerForDraggedTypes:
[NSArray arrayWithObjects:NSTIFFPboardType,NSFilenamesPboardType,nil]];
[super drawRect:dirtyRect];
}
- (NSDragOperation)draggingEntered:(id )sender
{
if ((NSDragOperationGeneric & [sender draggingSourceOperationMask])
== NSDragOperationGeneric)
{
return NSDragOperationGeneric;
} // end if
// not a drag we can use
return NSDragOperationNone;
}
- (BOOL)prepareForDragOperation:(id )sender
{
return YES;
}
#end
Now Drag and Drop a NSBox on the Xib and the Modify the class of NSBox to NSModifiedBox.
Set a break point to the method "draggingEntered".
Now Drag a ".png" or ".gif" file and drop on the NSModifiedBox and you see the "draggingEntered" will get invoked
Or you can check by using NSLog as well inside a "draggingEntered".
Hope my answer will help you :)

Drag & drop (<NSDraggingDestination>) in Cocoa does not work

I want to implement some "simple" drag & drop into my Mac application.
I dragged in a view and entered "MyView" in my nib file accordingly.However I do not get any response in my console (I try to log messages whenever any of the protocol methods are triggered)
I have subclassed NSView like this:
#interface MyView : NSView <NSDraggingDestination>{
NSImageView* imageView;
}
and implement it like this:
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self registerForDraggedTypes:[NSImage imagePasteboardTypes]];
NSRect rect = NSMakeRect(150, 0, 400, 300);
imageView = [[NSImageView alloc] initWithFrame:rect];
[imageView setImageScaling:NSScaleNone];
[imageView setImage:[NSImage imageNamed:#"test.png"]];
[self addSubview:imageView];
}
return self;
}
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender{
NSLog(#"entered");
return NSDragOperationCopy;
}
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender{
return NSDragOperationCopy;
}
- (void)draggingExited:(id <NSDraggingInfo>)sender{
NSLog(#"exited");
}
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender{
NSLog(#"som");
return YES;
}
- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
NSPasteboard *pboard = [sender draggingPasteboard];
if ( [[pboard types] containsObject:NSURLPboardType] ) {
NSURL *fileURL = [NSURL URLFromPasteboard:pboard];
NSLog(#"%#", fileURL);
}
return YES;
}
- (void)concludeDragOperation:(id <NSDraggingInfo>)sender{
NSLog(#"conclude sth");
}
- (void)draggingEnded:(id <NSDraggingInfo>)sender{
NSLog(#"ended");
}
- (BOOL)wantsPeriodicDraggingUpdates{
NSLog(#"wants updates");
}
- (void)updateDraggingItemsForDrag:(id <NSDraggingInfo>)sender NS_AVAILABLE_MAC(10_7){
}
If anyone still needs this, using:
[self registerForDraggedTypes:[NSArray arrayWithObjects:
NSFilenamesPboardType, nil]];
instead of:
[self registerForDraggedTypes:
[NSImage imagePasteboardTypes]]; // BAD: dropped image runs away, no draggingdestination protocol methods sent...
solved this problem for me. I suspect this works because my XIB is targeting osX 10.5. NSFilenamesPboardType represents a 10.5-and-previous 'standard data' UTI and [NSImage imagePasteboardTypes] returns 10.6-and-after standard data UTIs.
By the way, in - (BOOL)performDragOperation:(id NSDraggingInfo)sender, you can get a path for the file that was dropped with:
NSArray *files = [pboard propertyListForType:NSFilenamesPboardType];
Got the solution here:
http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/DragandDrop/Tasks/acceptingdrags.html#//apple_ref/doc/uid/20000993-BABHHIHC

Accessing Button in NIB that is in my framework

I am trying to create a simple framework with a nib that has a button on it which can be customized (selector wise and title wise.) for this, i did the following:
I added a property:
#property (nonatomic,retain) NSButton*accessoryButton;
and connected it to my outlet:
#synthesize accessoryButton = littleButton;
I then shared the instance as such:
+ (TestWindow *)sharedPanel
{
return sharedPanel ? sharedPanel : [[[self alloc] init] autorelease];
}
- (id)init
{
if (sharedPanel) {
[self dealloc];
} else {
sharedPanel = [super init];
}
return sharedPanel;
}
and load the nib:
if( !somewindow )
{
[NSBundle loadNibNamed: #"window" owner:nil];
}
[NSApp activateIgnoringOtherApps:YES];
[somewindow center];
[somewindow setLevel:NSModalPanelWindowLevel];
[somewindow makeKeyAndOrderFront:self];
When I however want to change the title for example from my sample project, it never works.
[TestWindow sharedPanel] setTitle:#"hi"]; //doesnt work
Here's my setTitle: method:
-(void)setTitle:(NSString *)buttonTitle
{
[[self accessoryButton] setTitle:buttonTitle];
[[self accessoryButton] display];
}
I don't get an error but nothing happens either. What am I missing?
Is the button nil at runtime? Are you sure your button's outlet is connected?
Does your init function get called when the NIB is loaded?

Drag and Drop folder view cocoa

I need to make a drag and drop view in cocoa that will accept folders. I know it will use things like NSView and probably registerForDraggedTypes: (which I still am not sure how to go about using). Does anyone know how to get this working?
Thanks in advance
Make a class called DragDropView that subclasses NSView and set the view in MainMenu.xib to be of this type (Select your view, go to Identity Inspecor and write DragDropView in Custom Class).
Write the code (see below) for DragDropView and Run it. An empty window should appear.
Drag some folders onto your window. You should get the paths of the folders written in your console. Something like.
2014-02-01 11:18:10.435 Start[41767:303] (
"/Users/bob/Desktop/Heathers Animations",
"/Users/bob/Desktop/bird.atlas"
)
// DragDropView.h
#import <Cocoa/Cocoa.h>
#interface DragDropView : NSView
#end
// DragDropView.m
#import "DragDropView.h"
#implementation DragDropView {
BOOL isHighlighted;
}
- (void)awakeFromNib {
[self registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, nil]];
}
- (BOOL)isHighlighted {
return isHighlighted;
}
- (void)setHighlighted:(BOOL)value {
isHighlighted = value;
[self setNeedsDisplay:YES];
}
- (void)drawRect:(NSRect)frame {
[super drawRect:frame];
if (isHighlighted) {
[NSBezierPath setDefaultLineWidth:6.0];
[[NSColor keyboardFocusIndicatorColor] set];
[NSBezierPath strokeRect:frame];
}
}
#pragma mark - Dragging
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender {
NSPasteboard *pboard = [sender draggingPasteboard];
if ([[pboard types] containsObject:NSFilenamesPboardType]) {
NSArray *paths = [pboard propertyListForType:NSFilenamesPboardType];
for (NSString *path in paths) {
NSError *error = nil;
NSString *utiType = [[NSWorkspace sharedWorkspace]
typeOfFile:path error:&error];
if (![[NSWorkspace sharedWorkspace]
type:utiType conformsToType:(id)kUTTypeFolder]) {
[self setHighlighted:NO];
return NSDragOperationNone;
}
}
}
[self setHighlighted:YES];
return NSDragOperationEvery;
}
- (void)draggingExited:(id <NSDraggingInfo>)sender {
[self setHighlighted:NO];
}
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender {
return YES;
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
[self setHighlighted:NO];
return YES;
}
- (void)concludeDragOperation:(id<NSDraggingInfo>)sender {
NSArray *files = [[sender draggingPasteboard] propertyListForType:NSFilenamesPboardType];
NSLog(#"%#", files);
}
#end
Most of what you need is in the drag and drop documentation, but what you need specifically is the NSFilenamesPboardType. It's an array if file paths.

Resources