0
votes

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.

1
I suggest reading Drag and Drop Programming Topics. Use dragImage:at:offset:event:pasteboard:source:slideBack:, NSDraggingSource and NSDraggingDestination. - Willeke
…Even though what I'm trying to do is drag an entire NSView, buttons, widgets and all, and not merely an NSImage? As I say, I've been following the tutorials (you may even have recognised some of the code as being from the tutorials!) but it doesn't seem to work! - headbanger
The dragged image is a representation of the data and the data can be anything: a text snippet, file, image, table row, url, …. - Willeke
Ahh. Thank you - I now have the first glimmers of working drag and drop. At least, the drag bit works - but the drop bit not so much. Any good hints for where to go next? I've also fixed the GitHub for the code so that it isn't a zip anymore. - headbanger
The drop bit works too, swap the views in performDragOperation. - Willeke

1 Answers

1
votes

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;
}