DragMatrix

last edited November 11, 2005 10:01:58 (200.121.26.125)
CocoaDev is sponsored by: Panic: Shockingly good Mac software!

I needed a matrix of image views that would let the user change the order of the image in it using drag and drop. Thus I wrote the following class. When the user drags an image a little insertion bar is draw to give them an indication of where the insertion will occur.

-- SaileshAgrawal


DragMatrix.h

/*****************************************************************************
 * DragMatrix
 *
 * This calls allows you to create a matrix of image cells that can be
 * reordered (similar to object in an outline view).  To use this class
 * add it to your project.  In InterfaceBuilder create a matrix of
 * ImageViews.  In the properties pane go to custom class and set the class
 * to DragMatrix.  When a image is dragged this class sends a notification
 * "DragMatrixImageMoved".  The object is itself.  The user info contains
 * the following keys
 * 	- "srcRow" - the row of the image being moved (NSNumber)
 * 	- "srcCol" - the column of the image being moved (NSNumber)
 * 	- "dstRow" - the row the image is being moved to (NSNumber)
 * 	- "dstCol" - the column the image is being moved to (NSNumber)
*****************************************************************************/

#import <AppKit/AppKit.h>

@interface DragMatrix : NSMatrix {
    NSEvent * downEvent;
    NSRect oldDrawRect, newDrawRect;
    BOOL shouldDraw;
    
    int srcRow, srcCol, dstRow, dstCol;
} 

// Private
- (NSEvent*) downEvent; 
- (void) setDownEvent:(NSEvent *)event; 
- (void) clearDragDestinationMembers;
@end

#import "DragMatrix.h"

float INS_WIDTH = 2;
float CIRCLE_SIZE = 6;
NSString *pasteBoardTypeCover = @"Cover";
        
/*****************************************************************************
 * Function - _scaledImage
 *
 * HACK.  This is used to access the scaled image of a imageCell.
 * When the user drags the image we want the scaled image not the full
 * size.
*****************************************************************************/
@implementation NSImageCell(DraggableImageView)
- (NSImage *)_scaledImage {
    return _scaledImage;
}
@end

@implementation DragMatrix

/*****************************************************************************
 * Function - initWithCoder
 *
 * Initialize and register ourself for drag operation.  Note since we use
 * a private user defined drag type (pasteBoardTypeCover) we will only
 * accept drags from within this app. This is called when we've been loaded
 * from an NIB.
*****************************************************************************/
- (id)initWithCoder:(NSCoder *)decoder {
    if (self = [super initWithCoder:decoder]) {
        [self registerForDraggedTypes:[NSArray arrayWithObjects:pasteBoardTypeCover, nil]];
    }
    return self;
}
 
/*****************************************************************************
 * Function - initWithFrame
 *
 * Initialize and register ourself for drag operation.  Note since we use
 * a private user defined drag type (pasteBoardTypeCover) we will only
 * accept drags from within this app.  This is called when we've been created
 * dynamically (as opposed to loaded from a NIB).
*****************************************************************************/
- (id)initWithFrame:(NSRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self registerForDraggedTypes:[NSArray arrayWithObjects:pasteBoardTypeCover, nil]];
    }
    return self;
}

/*****************************************************************************
 * Function - clearDragDestinationMembers
 *
 * Resets all member variables used by DragDestination functions.
*****************************************************************************/
- (void) clearDragDestinationMembers {
    shouldDraw = FALSE;
    oldDrawRect = NSZeroRect;
    newDrawRect = NSZeroRect;
}

/*****************************************************************************
 * Function - draggingEntered (implements NSDraggingDestination)
 *
 * Called when the user drags an object on top of us. The return value tells
 * the caller if we'll accept the object.
*****************************************************************************/
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender { 
    if ([sender draggingSource] == self) {
        [self clearDragDestinationMembers];
        return NSDragOperationAll;
    } else {
        return NSDragOperationNone;
    }
}

/*****************************************************************************
 * Function - performDragOperation (implements NSDraggingDestination)
 *
 * Called after the user releases the drag object.  Here we perform the
 * result of the dragging.
*****************************************************************************/
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
    NSPasteboard *pboard;
    NSArray *types;

    pboard = [sender draggingPasteboard];
    types = [pboard types];
    if ([types indexOfObject:pasteBoardTypeCover] != NSNotFound) {
        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
	NSString *keys[4] = {@"srcRow", @"srcCol", @"dstRow", @"dstCol"};
        NSNumber *objects[4];
        NSDictionary *dict;
        
        objects[0] = [NSNumber numberWithInt:srcRow];
        objects[1] = [NSNumber numberWithInt:srcCol];
        objects[2] = [NSNumber numberWithInt:dstRow];
        objects[3] = [NSNumber numberWithInt:dstCol];
        dict = [NSDictionary dictionaryWithObjects:objects forKeys:keys count:4];
        [nc postNotificationName:@"DragMatrixImageMoved" object:self userInfo:dict];
    }
    
    [self clearDragDestinationMembers];
    [self setNeedsDisplay:TRUE];
    
    return YES;
}

/*****************************************************************************
 * Function - draggingExited (implements NSDraggingDestination)
 *
 * Invoked when the dragged image exits the destination's bounds rectangle.
 * We use this to erase the insertion pointer from the view.
*****************************************************************************/
- (void)draggingExited:(id <NSDraggingInfo>)sender {
    [self clearDragDestinationMembers];
    [self setNeedsDisplay:TRUE];
}

/*****************************************************************************
 * Function - draggingUpdated (implements NSDraggingDestination)
 *
 * Invoked periodically as the image is held within the destination.
 * The messages continue until the image is either released or dragged out of
 * the window or view.
 *
 * To give the user feedback about where the image will be droped we
 * draw a little insertion point.
*****************************************************************************/
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender {
    NSPoint point;
    NSSize cellSize, cellSpacing;
    NSRect drawRect, cellRect;;
    int row, column;
    float offsetx;

    if ([sender draggingSource] != self)
        return NSDragOperationNone;
    
    // Note that the matrix coordiante system is flipped such that the
    // origin is located on the top left (as opposed to bottom left).
    point = [self convertPoint:[sender draggingLocation] fromView:nil];

    cellSize = [self cellSize];
    cellSpacing = [self intercellSpacing];

    if (point.y < cellSize.height) {
        row = 0; 
    } else {
        row = (point.y-cellSize.height)/(cellSize.height+cellSpacing.height) + 1;
    }
    if (point.x < cellSize.width) {
        column = 0;
    } else {
        column = (point.x-cellSize.width)/(cellSize.width+cellSpacing.width) + 1;
    }

    cellRect = [self cellFrameAtRow:row column:column];
    offsetx = cellSpacing.width/2 - INS_WIDTH/2;
    drawRect.size.height = cellRect.size.height;
    drawRect.size.width = INS_WIDTH;
    drawRect.origin.y = cellRect.origin.y;
    if (point.x < cellRect.origin.x + cellSize.width/2) {
        // insert to the left
        if (column == 0) {
            drawRect.origin.x = cellRect.origin.x;
        } else {
            // HACK - I just nudge it 2 pixels to the left to make it
            // centered.  I have no idea what the correct way to do this is
            drawRect.origin.x = cellRect.origin.x - offsetx - 2;
        }
        dstCol = column;
    } else {
        // insert to the right
        if (column == [self numberOfColumns] - 1) {
            drawRect.origin.x = cellRect.origin.x + cellRect.size.width - 2;
        } else {
            drawRect.origin.x = cellRect.origin.x + cellRect.size.width + offsetx;
        }
        dstCol = column+1;
    }

    shouldDraw = TRUE;
    dstRow = row;
    
    // Don't ask the view to draw it self unless necessary
    if (NSEqualRects(drawRect, oldDrawRect) == FALSE) {
        newDrawRect = drawRect;
        [self setNeedsDisplay:TRUE];
    }

    return NSDragOperationAll;   
}

/*****************************************************************************
 * Function - drawRect
 *
 * We override drawRect so we can draw our insertion pointer to indicate where
 * the drag will occur.
*****************************************************************************/
- (void) drawRect:(NSRect)rect {
    [super drawRect:rect];
    if (shouldDraw) {
        NSRect rect;
        
        shouldDraw = TRUE;
        [[NSColor blackColor] set];
        [NSBezierPath fillRect:newDrawRect];
        
        rect.size.width = CIRCLE_SIZE;
        rect.size.height = CIRCLE_SIZE;
        rect.origin.x = newDrawRect.origin.x + INS_WIDTH/2 - CIRCLE_SIZE/2;
        rect.origin.y = newDrawRect.origin.y - CIRCLE_SIZE;
        [[NSBezierPath bezierPathWithOvalInRect:rect] stroke];
        
        oldDrawRect = newDrawRect;
    }
}

/*****************************************************************************
 * Function - startDrag
 *
 * Private function.  We use this to start the drag operation.
*****************************************************************************/
- (void)startDrag:(NSEvent *)event { 
    NSPasteboard *pb = [NSPasteboard pasteboardWithName: NSDragPboard]; 
    NSImage *scaledImage, *dragImage;
    NSSize size; 
    NSPoint dragPoint, pt; 
    NSRect theDraggedCellFrame; 
    
    pt = [self convertPoint:[event locationInWindow] fromView:nil];
    [self getRow:&srcRow column:&srcCol forPoint:pt]; 
    // Note: _scaledImage is function we add to NSImageCell in our category.
    scaledImage = [[self cellAtRow:srcRow column:srcCol] _scaledImage]; 
    
    theDraggedCellFrame = [self cellFrameAtRow:srcRow column:srcCol]; 
    size = [scaledImage size];
    dragPoint.x = theDraggedCellFrame.origin.x 
        + ([self cellSize].width - size.width) / 2;
    dragPoint.y = theDraggedCellFrame.origin.y + size.height
        + ([self cellSize].height - size.height) / 2;

    [pb declareTypes: [NSArray arrayWithObjects: pasteBoardTypeCover, nil] owner: self]; 
    [pb setData:nil forType:pasteBoardTypeCover]; 

    // we want to make the image a little bit transparent so the user can see where
    // they're dragging to
    dragImage = [[[NSImage alloc] initWithSize: [scaledImage size]] autorelease]; 
    [dragImage lockFocus]; 
    [scaledImage dissolveToPoint: NSMakePoint(0,0) fraction: .5]; 
    [dragImage unlockFocus]; 

    [self dragImage: dragImage 
                 at: dragPoint 
             offset: NSMakeSize(0,0) 
              event: event 
         pasteboard: pb 
             source: self 
          slideBack: YES]; 
}


/*****************************************************************************
 * Function - shouldDelayWindowOrderingForEvent:
 *
 * Private function.  We use this to start the drag operation.
*****************************************************************************/
- (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent *)event { 
    // maybe make more discerning?! 
    return YES; 
} 

/*****************************************************************************
 * Function - acceptsFirstMouse:
 *
 * Private function.  We use this to start the drag operation.
*****************************************************************************/
- (BOOL)acceptsFirstMouse:(NSEvent *)event { 
    return YES; 
} 

/*****************************************************************************
 * Function - mouseDown:
 *
 * Private function.  We use this to start the drag operation.
*****************************************************************************/
- (void)mouseDown:(NSEvent *)event { 
    [self setDownEvent:event];
}

/*****************************************************************************
 * Function - mouseUp:
 *
 * Private function.  We use this to start the drag operation.
*****************************************************************************/
- (void)mouseUp:(NSEvent *)event {
    BOOL cellWasHit;
    int row, column;
    NSPoint point;
    
    point = [self convertPoint:[event locationInWindow] fromView:nil];
    cellWasHit = [self getRow:&row column:&column forPoint:point];
    if (!cellWasHit) {
        [super mouseUp:event];
        return;
    }
    
    if ([event modifierFlags] & NSCommandKeyMask) {
        int r,c, s, i, i2;
        r = [self selectedRow];
        c = [self selectedColumn];
        s = [self numberOfColumns];
        i = r*s + c;
        i2 = row*s + column;
        [self setSelectionFrom:i2 to:i2 anchor:i2 highlight:YES];     
    } else if ([event modifierFlags] & NSShiftKeyMask) {
        int r,c, s, i, i2;
        r = [self selectedRow];
        c = [self selectedColumn];
        s = [self numberOfColumns];
        i = r*s + c;
        i2 = row*s + column;
        [self setSelectionFrom:i to:i2 anchor:i highlight:YES];
    } else {
        [self selectCellAtRow:row column:column];
    }
}

/*****************************************************************************
 * Function - mouseDragged:
 *
 * If we hit a cell, then start the drag 
*****************************************************************************/
- (void)mouseDragged:(NSEvent *)event { 
    int row, column; 
    NSPoint point;

    point = [self convertPoint:[event locationInWindow] fromView:nil];
    if ([self getRow:&row column:&column forPoint:point]) { 
        [self startDrag:downEvent]; 
    } 
    [self setDownEvent:nil]; 
} 

/*****************************************************************************
 * Function - downEvent
 *
 * Return the mouse down event.
*****************************************************************************/
- (NSEvent *)downEvent { 
    return downEvent; 
} 

/*****************************************************************************
 * Function - setDownEvent:
 *
 * Set the mouse down event.
*****************************************************************************/
- (void)setDownEvent:(NSEvent *)event { 
    [downEvent autorelease]; 
    downEvent = [event retain]; 
} 

@end


Thank you for this class. You have made my week. *tips hat*


Amen to that! This is almost exactly what I was looking for, and certainly more than I had hoped for in an example. Thanks! -- Seth. 2005-03-28


2005-02-18 Changed dstCol = column++; to dstCol = column + 1; in the above. Someone forgot how post-increment works :)