Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Drag-to-resize NSView (or other object)

I'm trying to build an app that will allow the user to specify multiple areas of an image using rectangular bounding boxes that they can resize.

So far, I've got an NSScrollView that contains an NSImageView so the user can zoom in on the image and scroll around as they desire. My current thinking is that I can use NSViews as a way to provide a bounding box that the user can position and resize to cover the desired area, convert the NSView frames into percentages of the image size, and then store those values for later use.

There's an addAreaToImage method that adds an NSView to the NSScrollView at the center of wherever the user is currently looking. What I want is for the user to then be able to click and drag on the corners of the area to resize/move it wherever they want it to be. Sort of a live bounding box, if you will.

After reading through the documentation, most of the things related to dragging are about making the NSView a place to drag something else (like an image) or resizing due to the superview being resized, neither of which are what I'm looking to do.

My fear is that the answer to this problem (or the set of answers that would lead to me being able to roll my own solution) are so basic that no one ever thinks about them, which the last few days of Googling have pretty much confirmed for me.

I'm coming from iOS development, so this isn't completely new territory, but NSView and UIView seem to have enough differences to confuse me thoroughly so far.

like image 922
Carter Fort Avatar asked Oct 14 '13 12:10

Carter Fort


2 Answers

Yes, you will need to implement it yourself but it isn't overly complicated.

First, you need to make some decisions about how you want your area views to behave and look like. Do you need just resize or be able to drag (move) the views as well? How are they drawn when they are passive/dragged/resized/highlighted. Do you want to have a resize and drag cursors? What is the behavior of the resizing, just drag a corner or all the borders? What's the drag border width?

You then subclass the NSView that you are using as your area views. Give it some private members to indicate its states (like isDragged, isResized etc).

Implement drawRect: to draw the view. Taking into account it various states (e.g you probably want to visualize when it is being dragged or resized, draw a transparent overlay, etc).

Next you want to handle mouse events by implementing mouseDown:, mouseDragged:, mouseUp: and maybe mouseMoved:. Here your resize/drag logic will be placed. Check where the user initially clicked in mouseDown: and decide what operations are possible from that point setting the relevant states. Follow up in mouseDragged: to perform the operation (by setting the view's frame origin and size accordingly). Finalize the operation in mouseUp: (validate, set states, invoke done logic, register undo operation)

When dealing with points and rects, don't forget about the coordinate system. You will need to translate them to/from views and base system. NSView has all the methods needed for this.

You need to call setNeedsDisplay: or setNeedsDisplayInRect: each time you want the view to redraw itself to reflect the changes in size and position.

You may also want to use Tracking Areas for areas in your view that need a different cursor (e.g. resize cursor on the corner).

When dragging/resizing don't forget to implement logic for responding to user dragging the mouse out of the parent's view bounds.

By the way, why are you adding your views to the scrollview? I think they are better placed as subviews of the imageview (if possible) or clipview so they can be scrolled.

like image 185
danielv Avatar answered Oct 15 '22 01:10

danielv


I was also in similar situation and here is my solution to resize NSView from corners.I think you can modify same according to your requirements.

import Cocoa
import Foundation

enum CornerPosition {
    case topLeft, topRight, bottomRight, bottomLeft, none
}

class DraggableResizableView1: NSView {

private let resizableArea: CGFloat = 5
private var cursorPosition: CornerPosition = .none

override init(frame frameRect: NSRect) {
    super.init(frame: frameRect)
    self.frame = self.frame.insetBy(dx: -2, dy: -2);
    self.wantsLayer = true
    self.layer?.backgroundColor = NSColor.yellow.cgColor
    self.layer?.borderWidth = 2
    self.layer?.borderColor = NSColor.blue.cgColor
}

override func updateTrackingAreas() {
    super.updateTrackingAreas()
    trackingAreas.forEach { area in
        removeTrackingArea(area)
    }
    addTrackingRect(bounds)
}

required init?(coder decoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

override func mouseExited(with event: NSEvent) {
    super.mouseExited(with: event)
    NSCursor.arrow.set()
}

override func mouseDown(with event: NSEvent) {
    super.mouseDown(with: event)
    let locationInView = convert(event.locationInWindow, from: nil)
    cursorPosition = cursorCornerPosition(locationInView)
}

override func mouseUp(with event: NSEvent) {
    super.mouseUp(with: event)
    cursorPosition = .none
}

override func mouseMoved(with event: NSEvent) {
    super.mouseMoved(with: event)
    let locationInView = convert(event.locationInWindow, from: nil)
    cursorCornerPosition(locationInView)
}

override func mouseDragged(with event: NSEvent) {
    super.mouseDragged(with: event)
        
    let deltaX = event.deltaX
    let deltaY = event.deltaY
    guard let superView = superview else { return }
    
    switch cursorPosition {
    case .topLeft:
        if frame.size.width - deltaX > superview!.frame.width/5 && frame.size.width - deltaX < superview!.frame.width/2 &&  frame.origin.x + deltaX >= superView.frame.minX && (superView.frame.height - (frame.size.width-deltaX)*9/16) > frame.minY {
            frame.origin.x    += deltaX
            frame.origin.y = frame.origin.y
            frame.size.width  -= deltaX
            frame.size.height =  frame.size.width*9/16
        }
    case .bottomLeft:
        if frame.size.width - deltaX > superview!.frame.width/5 && frame.size.width - deltaX < superview!.frame.width/2 && frame.origin.x + deltaX > 0 && frame.origin.x + deltaX >= superView.frame.minX && frame.origin.y + deltaX*9/16 >  superView.frame.minY {
            frame.origin.x    += deltaX
            frame.origin.y    += deltaX*9/16
            frame.size.width  -= deltaX
            frame.size.height =  frame.size.width*9/16
        }
    case .topRight:
        if frame.size.width + deltaX > superview!.frame.width/5 && frame.size.width + deltaX < superview!.frame.width/2 && (superView.frame.height - (frame.size.width+deltaX)*9/16) > frame.minY  && (superView.frame.width - (frame.size.width+deltaX)) > frame.minX  {
            frame.origin.x = frame.origin.x
            frame.origin.y = frame.origin.y
            frame.size.width  += deltaX
            frame.size.height =  frame.size.width*9/16
        }
    case  .bottomRight:
        if frame.size.width + deltaX > superview!.frame.width/5 && frame.size.width + deltaX < superview!.frame.width/2 && (superView.frame.width - (frame.size.width+deltaX)) > frame.minX && frame.origin.y - deltaX*9/16 > superView.frame.minY {
            frame.origin.x = frame.origin.x
            frame.origin.y -= deltaX*9/16
            frame.size.width  += deltaX
            frame.size.height =  frame.size.width*9/16
        }
    case .none:
        frame.origin.x    += deltaX
        frame.origin.y    -= deltaY
    }
    repositionView()


}

@discardableResult
func cursorCornerPosition(_ locationInView: CGPoint) -> CornerPosition {
    
    if (locationInView.y < resizableArea && bounds.width-locationInView.x < resizableArea) || (locationInView.x < resizableArea && bounds.height-locationInView.y < resizableArea) {
        NSCursor(image: NSImage(byReferencingFile: "/System/Library/Frameworks/WebKit.framework/Versions/Current/Frameworks/WebCore.framework/Resources/northWestSouthEastResizeCursor.png")!, hotSpot: NSPoint(x: 8, y: 8)).set()
        if locationInView.y < resizableArea && bounds.width-locationInView.x < resizableArea {
            return .bottomRight
        } else {
            return .topLeft
        }
    } else if (bounds.height-locationInView.y < resizableArea && bounds.width-locationInView.x < resizableArea) || (locationInView.x < resizableArea && locationInView.y < resizableArea) {
        NSCursor(image: NSImage(byReferencingFile: "/System/Library/Frameworks/WebKit.framework/Versions/A/Frameworks/WebCore.framework/Versions/A/Resources/northEastSouthWestResizeCursor.png")!, hotSpot: NSPoint(x: 8, y: 8)).set()
        if bounds.height-locationInView.y < resizableArea && bounds.width-locationInView.x < resizableArea {
            return .topRight
        } else {
            return .bottomLeft
        }
    }
    else {
        NSCursor.openHand.set()
        return .none
    }
}

private func repositionView() {
    if frame.minX < 0 {
        frame.origin.x    = 0
    }
    if frame.minY < 0 {
        frame.origin.y    = 0
    }
    guard let superView = superview else { return }
    if frame.maxX > superView.frame.maxX {
        frame.origin.x    = superView.frame.maxX - frame.size.width
    }
    if frame.maxY > superView.frame.maxY {
        frame.origin.y    = superView.frame.maxY - frame.size.height
    }
}
}


extension NSView {
    
 func addTrackingRect(_ rect: NSRect) {
        addTrackingArea(NSTrackingArea(
            rect: rect,
            options: [
                .mouseMoved,
                .mouseEnteredAndExited,
                .activeAlways],
            owner: self))
    }
}
like image 30
Akhil Shrivastav Avatar answered Oct 15 '22 00:10

Akhil Shrivastav