headbanger headbanger - 3 months ago 22
Objective-C Question

Drag swap NSViews

Because you never know, it might be useful, I've been trying to write a flexible NSSplitView experimental app where views can be added, and removed, on the fly in any way that the user wants. That bit I can do.

Now I'm thinking that it would be useful to be able to:


  1. swap views around - so that, for example, in a four view window the top left view could be dragged to bottom right and, on release of the mouse button, the views would swap with each other.

  2. drag views out - so that, for example, in a four view window if the top left view is dragged out of it's containing window then it will become a window in its own right containing that view, and the original window will become a three view window.

  3. drag views in - so that a window can be dragged into a view, closing the window and adding its view to the window that it was dragged into.



I've written a program doing the first bit (flexible set up of split views) https://github.com/HeadBanging/SplitViewTest but I'm at a total loss how to do the rest - particularly point one.

If you take a look at the code, you can see that I've made a start (using the tutorials from Apple and elsewhere), but it doesn't do what I want. Does anyone have any suggestions?

Of course, if all you need is a flexible split window for your project then here you go - have mine (download above), no restrictions on use - and all the best.

Willeke had some good suggestions for how to get the drag working, which I've implemented as follows (full code on Git):

#pragma mark Dragging

- (NSImage *)imageRepresentationOfView:(NSView*)draggingView {
BOOL wasHidden = draggingView.isHidden;
CGFloat wantedLayer = draggingView.wantsLayer;

draggingView.hidden = NO;
draggingView.wantsLayer = YES;

NSImage *image = [[NSImage alloc] initWithSize:draggingView.bounds.size];
[image lockFocus];
CGContextRef ctx = [NSGraphicsContext currentContext].graphicsPort;
[draggingView.layer renderInContext:ctx];
[image unlockFocus];

draggingView.wantsLayer = wantedLayer;
draggingView.hidden = wasHidden;

return image;
}

- (void)mouseDown:(NSEvent *)theEvent {
NSSize dragOffset = NSMakeSize(0.0, 0.0);
NSPasteboard *pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
[pboard declareTypes:[NSArray arrayWithObject:NSTIFFPboardType] owner:self];

DebugView *hitView;
NSPoint startLocation = NSMakePoint(0, 0);
NSImage *draggedImage;
BOOL found = NO;

fHitView = nil;
while ((hitView = [[[self subviews] objectEnumerator] nextObject]) && !found) {
if ([hitView isKindOfClass:[DebugView class]] && [(DebugView *)hitView dragEnabled]) { //Change DebugView to Draggable View, and use as container for plugin views
draggedImage = [self imageRepresentationOfView:hitView];
startLocation = hitView.frame.origin;
found = YES;
}
}
if (draggedImage != nil) {
[pboard setData:[draggedImage TIFFRepresentation] forType:NSTIFFPboardType];

[self dragImage:draggedImage at:startLocation offset:dragOffset
event:theEvent pasteboard:pboard source:self slideBack:YES];
}
return;
}

- (void)setHighlighted:(BOOL)value {
isHighlighted = value;
[self setNeedsDisplay:YES];
}

- (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];

DebugView *hitView;
BOOL found = NO;

fHitView = nil;
while ((hitView = [[[self subviews] objectEnumerator] nextObject]) && !found) {
if ([hitView isKindOfClass:[DebugView class]] && [(DebugView *)hitView dragEnabled]) {
found = YES;
}
}
NSView* tempView = [sender draggingSource];
[[[sender draggingSource] superview] replaceSubview:[sender draggingSource] with:hitView];

[self replaceSubview:hitView with:tempView];
[self setNeedsDisplay:YES];
[[[sender draggingSource] superview] setNeedsDisplay:YES];
return YES;
}

- (BOOL)isHighlighted {
return isHighlighted;
}


The dropping part partly works - some of the time the view prepares to accept the drop, some of the time it doesn't (anyone see what I'm doing wrong? - it should work all the time, except when the view being dropped onto is the source view).

The final piece of the puzzle is still a mystery to me (accepting the drop, and swapping the views over). Any hints would be very gratefully accepted.

Answer

A view can have one superview, when you add a view to another superview, it is removed from the original superview. Replacing view A by view B and then view B by view A is not going to work because view B is already removed from its original superview.

Autolayout is still a mystery to me but removing both views first and then adding them both seems to work:

- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
    [self setHighlighted:NO];

    // swap subviews of view1 and view2
    NSView *view1 = self;
    NSView *view2 = [sender draggingSource];

    // find subviews
    DebugView *hitView1, *hitView2;
    for (hitView1 in [view1 subviews]) {
        if ([hitView1 isKindOfClass:[DebugView class]]) {
            break;
        }
    }
    for (hitView2 in [view2 subviews]) {
        if ([hitView2 isKindOfClass:[DebugView class]]) {
            break;
        }
    }

    // swap hitView1 and hitView2
    if (hitView1 && hitView2) {
        [hitView1 removeFromSuperview];
        [hitView2 removeFromSuperview];
        [view1 addSubview:hitView2];
        NSDictionary *views = NSDictionaryOfVariableBindings(hitView2);
        [view1 addConstraints:
            [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[hitView2]|"
                                                    options:0
                                                    metrics:nil
                                                      views:views]];
        [view1 addConstraints:
            [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[hitView2]|"
                                                    options:0
                                                    metrics:nil
                                                      views:views]];
        [view2 addSubview:hitView1];
        views = NSDictionaryOfVariableBindings(hitView1);
        [view2 addConstraints:
            [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[hitView1]|"
                                                    options:0
                                                    metrics:nil
                                                      views:views]];
        [view2 addConstraints:
            [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[hitView1]|"
                                                    options:0
                                                    metrics:nil
                                                      views:views]];
        return YES;
    }
    return NO;
}