Re: NSTreeController drag and drop
Re: NSTreeController drag and drop
- Subject: Re: NSTreeController drag and drop
- From: Keith Blount <email@hidden>
- Date: Wed, 27 Jul 2005 03:01:15 -0700 (PDT)
SA Dev -
I will do my best, if it helps. What follows is an
in-depth description of what I tried to get
drag-and-drop working with NSTreeController.
I created my own CONode class for storing the items in
my outline view. Each node has an NSMutableArray
*children instance variable to hold child nodes and an
isLeaf BOOL variable to check whether it should be
expandable or not. It also has several convenience
methods such as -(BOOL)isDescendantOfNodes:(NSArray
*)nodes and -(BOOL)isDescendantOfOrOneOfNodes:(NSArray
*)nodes, which assist in calculating whether items
should be dropped, and -
(void)removeObectFromChildren:(id)object, which looks
through all the descendants of the receiver looking to
delete the passed-in object - these methods are
well-tested and work fine. My main document has an
NSMutableArray *contents instance variable that holds
the root nodes.
Here is my acceptDrop datasource method for an outline
view that *doesn't* use NSTreeController. This method
works perfectly:
- (BOOL)outlineView:(NSOutlineView*)ov acceptDrop:(id
<NSDraggingInfo>)info item:(id)targetItem
childIndex:(int)index
{
int i, n;
NSPasteboard *pboard = [info draggingPasteboard]; //
Get the pasteboard
// Check the draggind type
if ([pboard availableTypeFromArray:[NSArray
arrayWithObject:CONodesPboardType]])
{
// Read the data
NSData *data = [pboard
dataForType:CONodesPboardType];
NSArray *newNodes = [NSKeyedUnarchiver
unarchiveObjectWithData:data];
NSMutableArray *targetArray = targetItem ?
[targetItem children] : contents;
// Add the new items (we do this backwards,
otherwise they will end up in reverse order)
for (i = ([newNodes count]-1); i >=0; i--)
{
// We only want to copy in each item in the array
once - if a folder
// is open and the folder and its contents were
selected and dragged,
// we only want to drag the folder, of course.
if (![[newNodes objectAtIndex:i]
isDescendantOfNodes:newNodes])
[targetArray insertObject:[newNodes
objectAtIndex:i] atIndex:index];
}
// Now delete the originals if dragged from self
if ([info draggingSource] == outlineView)
{
for (i = 0; i < [draggedNodes count]; i++)
{
CONode *dragItem = [draggedNodes objectAtIndex:i];
// First, try deleting them from the root folder
if ([contents containsObject:dragItem])
[contents removeObject:dragItem];
else
{
// In case this didn't work, check all the
subfolders
for (n = 0; n < [contents count]; n++)
{
if(![[contents objectAtIndex:n] isLeaf])
{
if ([dragItem
isDescendentOfOrOneOfNodes:[[contents objectAtIndex:n]
children]])
{
[[contents objectAtIndex:n]
removeObjectFromChildren:[draggedNodes
objectAtIndex:i]];
break;
}
}
}
}
}
}
[outlineView reloadData];
return YES;
}
return NO;
}
Here is the same method used with an outline view that
*does* use an NSTreeController. Notice that the only
modification is that it now has to convert the
targetItem that is passed in to an -observedObject,
because when using an NSTreeController the items
passed in are always of private class
_NSArrayControllerTreeNode. I have an outline view
subclass with an -observedObjectForItem: method for
exactly this purpose. According to Scott Stevenson's
tips on his page, this is all that should be necessary
to get it working (obviously I am implementing dummy
datasource methods, also following Scott Stevenson's
excellent tips):
- (BOOL)outlineView:(NSOutlineView*)ov acceptDrop:(id
<NSDraggingInfo>)info item:(id)targetItem
childIndex:(int)index
{
int i, n;
NSPasteboard *pboard = [info draggingPasteboard]; //
Get the pasteboard
// Convert the _NSArrayControllerTreeNode item to
something useful
CONode *targetNode = [outlineView
observedObjectForItem:targetItem];
// Check the draggind type
if ([pboard availableTypeFromArray:[NSArray
arrayWithObject:CONodesPboardType]])
{
// Read the data
NSData *data = [pboard
dataForType:CONodesPboardType];
NSArray *newNodes = [NSKeyedUnarchiver
unarchiveObjectWithData:data];
NSMutableArray *targetArray = targetItem ?
[targetNode children] : contents;
// Add the new items (we do this backwards,
otherwise they will end up in reverse order)
for (i = ([newNodes count]-1); i >=0; i--)
{
// We only want to copy in each item in the array
once - if a folder
// is open and the folder and its contents were
selected and dragged,
// we only want to drag the folder, of course.
if (![[newNodes objectAtIndex:i]
isDescendantOfNodes:newNodes])
[targetArray insertObject:[newNodes
objectAtIndex:i] atIndex:index];
}
// Now delete the originals if dragged from self
if ([info draggingSource] == outlineView)
{
for (i = 0; i < [draggedNodes count]; i++)
{
CONode *dragItem = [draggedNodes objectAtIndex:i];
// First, try deleting them from the root folder
if ([contents containsObject:dragItem])
[contents removeObject:dragItem];
else
{
// In case this didn't work, check all the
subfolders
for (n = 0; n < [contents count]; n++)
{
if(![[contents objectAtIndex:n] isLeaf])
{
if ([dragItem
isDescendentOfOrOneOfNodes:[[contents objectAtIndex:n]
children]])
{
[[contents objectAtIndex:n]
removeObjectFromChildren:[draggedNodes
objectAtIndex:i]];
break;
}
}
}
}
}
}
[outlineView reloadData];
return YES;
}
return NO;
}
As you can see, I'm doing nothing different here at
all. The only difference is that there is now an
NSTreeController present and I am now manipulating its
underlying data directly. But now the method does not
work as expected. The outline view goes out of sync. I
may drop items only to find that nothing happens, then
try again and two will appear. I tried all sorts of
things to get the NSTreeController back in sync. For a
start, I tried calling [outlineView reloadData] or
[outlineView reloadItem:targetNode reloadChildren:YES]
every time I added or deleted an object. This seemed
to help at first, but after extensive testing I would
find that some items had copied themselves and others
had disappeared again. I also tried calling
-willChangeValueForKey: and -didChangeValueForKey: on
the objects holding the array I was modifying whenever
I made an alteration to an array, so that the
NSTreeController had chance to observe all changes and
react. But this led to random SIGBUS crashes
(apparently on [self
didChangeValueForKey:@"contents"]) and I still had
strange results. For one thing, when adding objects to
the root, they always seemed to get added to the
bottom of the outline view no matter where I had tried
to insert them.
After trying everything I could think of to get the
NSTreeController to stay in sync with my modified
underlying data (I even tried calling [treeController
rearrangeObjects]) - and I really have no idea why
this should be such a problem when this works fine
when working with a plain datasource - I returned to
trying to use NSTreeController methods to handle all
of the insertion and deletion. It would make sense to
use NSTreeController insert and remove methods to do
all the work, as then you should be guaranteed for
everything to work smoothly - but NSTreeController
lacks the methods to do this. For instance,
NSTreeController really needs an
-arrangedIndexPathForObject: method to help find the
index paths of the objects you want to manipulate. The
only way to calculate an index path is to select an
object first - for which you need to use the outline
view methods. Anyway, I tried replacing my insertion
code in the above method with this code:
- (BOOL)outlineView:(NSOutlineView*)ov acceptDrop:(id
<NSDraggingInfo>)info item:(id)targetItem
childIndex:(int)index
{
int i, n;
NSPasteboard *pboard = [info draggingPasteboard]; //
Get the pasteboard
// Check the draggind type
if ([pboard availableTypeFromArray:[NSArray
arrayWithObject:CONodesPboardType]])
{
// Read the data
NSData *data = [pboard
dataForType:CONodesPboardType];
NSArray *newNodes = [NSKeyedUnarchiver
unarchiveObjectWithData:data];
NSIndexPath *indexPath;
if (targetItem)
{
[outlineView selectRowIndexes:[NSIndexSet
indexSetWithIndex:[outlineView rowForItem:targetItem]]
byExtendingSelection:NO];
indexPath = [[treeController selectionIndexPath]
indexPathByAddingIndex:index];
}
else
indexPath = [NSIndexPath indexPathWithIndex:index];
// Add the new items (we do this backwards,
otherwise they will end up in reverse order)
for (i = ([newNodes count]-1); i >=0; i--)
{
// We only want to copy in each item in the array
once - if a folder
// is open and the folder and its contents were
selected and dragged,
// we only want to drag the folder, of course.
if (![[newNodes objectAtIndex:i]
isDescendantOfNodes:newNodes])
[treeController insertObject:[newNodes
objectAtIndex:i] atArrangedObjectIndexPath:indexPath];
}
// The rest is the same as above
return YES;
}
return NO;
}
But this doesn't work properly either. Everything gets
inserted fine so long as we're not trying to insert
into the root. When inserting into the root, the
dragged node always gets placed at the bottom of the
outline view, no matter where you tried to insert it.
An NSLog shows that the index path is correct, and
seems to indicate that the node is getting inserted in
the right place and then shifted to the bottom of the
outline view straight afterwards. Another NSLog shows
that the node is inserted in the correct place in the
contents array - it just isn't getting displayed
correctly in the outline view. Obviously I tried
reloading the data and other things, but to no avail.
I didn't even figure out a way of removing the moved
objects using NSTreeController methods - that's even
more complicated.
Those are pretty much the results of my experiments in
trying to get NSTreeController to work with drag and
drop. This was just the last straw for me. Another big
problem with NSTreeController is that, because of the
proxy objects it passes to the datasource methods,
there is no way to get -itemForPersistentObject: to
work (because in the case of an NSTreeController, this
method expects to be passed back an
_NSArrayControllerTreeNode object, which is
impossible. (Well, maybe you could to through and
expand all of the items, then go through them all
seeing if any of their observed objects match the
persistent object, and then close them all again...
Maybe.) I posted an NSOutlineView subclass on CocoaDev
(http://www.cocoadev.com/index.pl?NSOutlineViewStateSavingWithNSTreeController)
that gets around this by providing methods for state
saving within your file.
So, after trying and trying to write workarounds to
get drop working, after writing an outline view
subclass to provide state saving that can be done
automatically and more elegantly by using a
non-NSTreeController outline view, and knowing that
you have to implement dummy datasource methods with
NSTreeController anyway (for DnD etc), I called it a
day. It is much, much less effort, time and code to
use the datasource and be done with it. Like I say, I
think NSTreeController is just a toddler at the
moment. Actually, I don't think it is well implemented
at all - it's notable that the only AppKit example
that uses it (OutlineEdit) does no state-saving or
drag and drop.
Anyway, I hope this explanation helps. if anyone
*does* have a solution for all of this, please, please
do share it. There is almost nothing out there about
NSTreeController at the moment. I am beginning to
think that nobody out there is actually using it,
because I'm sure I can't be the only person who will
come up against all of these problems.
All the best,
Keith
PS. If you are really interested in looking to this, I
would be happy to knock together a test project using
the above methods.
--- SA Dev <email@hidden> wrote:
> Keith:
>
> Can you provide us with a solid (bug report
> quality) description
> of the problem and, if possible, what you do to
> reproduce it even
> 'sometimes'? I think that'd help a great deal.
>
> The reason I ask is that I'm interested in this
> topic myself. If
> it's a problem I could run into, I want to make sure
> there's a way
> around it before I commit to going with this design.
>
> Thanks!
>
>
>
> On Jul 26, 2005, at 6:15 PM, Keith Blount wrote:
>
> > Thanks for the reply, Mike, much appreciated. I've
> > been using Scott's tips throughout, but in the
> final
> > analysis I found no way of getting drag and drop
> to
> > work without strange behaviour. It would seem to
> work
> > but then break in certain situations after a lot
> of
> > testing. Manipulating the data directly only
> seemed to
> > confused the tree controller, no matter what I
> did. I
> > eventually took a step back and realised that in
> order
> > to support drag and drop and expandable state
> saving
> > (because it's impossible to get
> > -outlineView:itemForPersistentObject: working with
> > NSTreeController), I had written more code hacking
> > around NSTreeController than I would have just by
> > implementing the datasource methods. So I have
> > returned to using a non-bindings outline view
> without
> > NSTreeController, and now everything works fine
> and,
> > ironically, I have less code.
> >
> > I think NSTreeController is a great start, but I
> think
> > it has a little way to go before it is really
> useful
> > for projects that want anything more than a very
> basic
> > outline view. I hope Apple do some more work on it
> - I
> > have filed a couple of enhancement requests in
> that
> > hope.
> >
> > Thanks again and all the best,
> > Keith
> >
> > --- Michael McCracken
> <email@hidden>
> > wrote:
> >
> >
> >> Keith, Scott Stevenson has some tips about this
> >> here:
> >> http://theocacao.com/document.page/130 , but it
> >> sounds like you've
> >> already gotten that far.
> >>
> >> I don't really think there's anything wrong with
> >> just manipulating the
> >> content directly, especially if that works and
> the
> >> code makes sense. I
> >> have had to do the same thing with
> NSTreeController,
> >> and as far as I
> >> can see it's really just a small step back
> towards
> >> pre-bindings cocoa,
> >> which wasn't all that bad.
> >>
> >> Although if anyone does have some insight about
> if
> >> you should always
> >> use the controllers' methods for manipulating
> >> content, and how to do
> >> it right, I'd be glad to hear it too.
> >>
> >> -mike
> >>
> >> On 7/26/05, Keith Blount <email@hidden>
> >> wrote:
> >>
> >>> Hello,
> >>>
> >>> I am just wondering if anyone has had any
> success
> >>> implementing drag and drop with
> NSTreeController.
> >>>
> >> I
> >>
> >>> have got it working fine for my app, but I want
> to
> >>> check that the way I am doing it is okay. It is
> >>>
> >> the
> >>
> >>> -outlineView:acceptDrop:item:childIndexes:
> method
> >>>
> >> that
> >>
> >>> I am unsure about. It seems to me that I should
> be
> >>> using NSTreeController's
> >>> -insertObject:atArrangedIndexPath: and
> >>> -removeObjectAtArrangedIndexPath: methods to
> >>>
> >> complete
> >>
> >>> my drop, but I have had less-than-reliable
> results
> >>> with these. For a start, I have found that
> >>> -insertObject:atArrangedIndexPath: behaves
> >>>
> >> erratically
> >>
> >>> when dropping on the root (ie. when the
> targetItem
> >>> passed in is NULL). It also seems impossible to
> >>>
> >> use
> >>
> >>> -removeObjectAtArrangedIndexPath: to remove
> >>>
> >> objects
> >>
> >>> that have been dragged (when the source and
> >>> destination are the same), because we need the
> >>>
> >> index
> >>
> >>> paths of all of the dragged items. This is
> >>>
> >> complicated
> >>
> >>> because: 1) These index paths may have changed
> >>>
> >> after
> >>
> >>> inserting the dropped items; 2) NSTreeController
> >>>
> >> only
> >>
> >>> provides methods for getting the index paths of
> >>> selected objects - I can't see any way to
> >>>
> >> calculate
> >>
> >>> the index paths from an array of dragged items.
> >>>
> >>> The only solution I have come up with is to
> >>>
> >> manipulate
> >>
> >>> the content arrays directly, without using any
> of
> >>> NSTreeController's methods, and then ensuring
> that
> >>>
> >> the
> >>
> >>> outline view remains up to date using
> >>> -reloadItem:reloadChildren: and -reloadData (if
> I
> >>> don't do this every time I add a new item, I get
> >>>
> >> some
> >>
> >>> very screwy results, which is only a problem
> when
> >>> using NSTreeController).
> >>>
> >>> Like I say, I have drag and drop working, but I
> >>>
> >> feel
> >>
> >>> that I may be doing it the wrong way because I
> am
> >>>
> >> not
> >>
> >>> using my tree controller to manage the drop. If
> >>>
>
=== message truncated ===
__________________________________________________
Do You Yahoo!?
Tired of spam? Yahoo! Mail has the best spam protection around
http://mail.yahoo.com
_______________________________________________
Do not post admin requests to the list. They will be ignored.
Cocoa-dev mailing list (email@hidden)
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden