'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.
Solution 1:[1]
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.
Solution 2:[2]
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))
}
}
Solution 3:[3]
Adding else directions(top, right, left, bottom) of resizing to Akhil Shrivastav posted code. And resizing not in a fixed ratio.
import Cocoa
enum CornerBorderPosition {
case topLeft, topRight, bottomRight, bottomLeft
case top, left, right, bottom
case none
}
class DraggableResizableView: NSView {
private let resizableArea: CGFloat = 5
private var cursorPosition: CornerBorderPosition = .none {
didSet {
switch self.cursorPosition {
case .bottomRight, .topLeft:
NSCursor(image:
NSImage(byReferencingFile: "/System/Library/Frameworks/WebKit.framework/Versions/Current/Frameworks/WebCore.framework/Resources/northWestSouthEastResizeCursor.png")!,
hotSpot: NSPoint(x: 8, y: 8)).set()
case .bottomLeft, .topRight:
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()
case .top, .bottom:
NSCursor.resizeUpDown.set()
case .left, .right:
NSCursor.resizeLeftRight.set()
case .none:
NSCursor.openHand.set()
}
}
}
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
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
trackingAreas.forEach({ removeTrackingArea($0) })
addTrackingArea(NSTrackingArea(rect: self.bounds,
options: [.mouseMoved,
.mouseEnteredAndExited,
.activeAlways],
owner: self))
}
override func mouseExited(with event: NSEvent) {
NSCursor.arrow.set()
}
override func mouseDown(with event: NSEvent) {
let locationInView = convert(event.locationInWindow, from: nil)
self.cursorPosition = self.cursorCornerBorderPosition(locationInView)
}
override func mouseUp(with event: NSEvent) {
self.cursorPosition = .none
}
override func mouseMoved(with event: NSEvent) {
let locationInView = convert(event.locationInWindow, from: nil)
self.cursorPosition = self.cursorCornerBorderPosition(locationInView)
}
override func mouseDragged(with event: NSEvent) {
guard let superView = superview else { return }
let deltaX = event.deltaX
let deltaY = event.deltaY
switch cursorPosition {
case .topLeft:
if superView.frame.width / 3 ..< superView.frame.width ~= self.frame.size.width - deltaX,
superView.frame.height / 3 ..< superView.frame.height ~= self.frame.size.height - deltaY,
self.frame.origin.x + deltaX >= superView.frame.minX,
self.frame.origin.y + self.frame.height - deltaY <= superView.frame.maxY {
self.frame.size.width -= deltaX
self.frame.size.height -= deltaY
self.frame.origin.x += deltaX
}
case .bottomLeft:
if superView.frame.width / 3 ..< superView.frame.width ~= self.frame.size.width - deltaX,
superView.frame.height / 3 ..< superView.frame.height ~= self.frame.size.height + deltaY,
self.frame.origin.x + deltaX >= superView.frame.minX,
self.frame.origin.y - deltaY >= superView.frame.minY {
self.frame.origin.x += deltaX
self.frame.origin.y -= deltaY
self.frame.size.width -= deltaX
self.frame.size.height += deltaY
}
case .topRight:
if superView.frame.width / 3 ..< superView.frame.width ~= self.frame.size.width + deltaX,
superView.frame.height / 3 ..< superView.frame.height ~= self.frame.size.height - deltaY,
self.frame.origin.x + self.frame.width + deltaX <= superView.frame.maxX,
self.frame.origin.y + self.frame.height - deltaY <= superView.frame.maxY {
self.frame.size.width += deltaX
self.frame.size.height -= deltaY
}
case .bottomRight:
if superView.frame.width / 3 ..< superView.frame.width ~= self.frame.size.width + deltaX,
superView.frame.height / 3 ..< superView.frame.height ~= self.frame.size.height + deltaY,
self.frame.origin.x + self.frame.width + deltaX <= superView.frame.maxX,
self.frame.origin.y - deltaY >= superView.frame.minY {
self.frame.origin.y -= deltaY
self.frame.size.width += deltaX
self.frame.size.height += deltaY
}
case .top:
if superView.frame.height / 3 ..< superView.frame.height ~= self.frame.size.height - deltaY,
self.frame.origin.y + self.frame.height - deltaY <= superView.frame.maxY {
self.frame.size.height -= deltaY
}
case .bottom:
if superView.frame.height / 3 ..< superView.frame.height ~= self.frame.size.height + deltaY,
self.frame.origin.y - deltaY >= superView.frame.minY {
self.frame.size.height += deltaY
self.frame.origin.y -= deltaY
}
case .left:
if superView.frame.width / 3 ..< superView.frame.width ~= self.frame.size.width - deltaX,
self.frame.origin.x + deltaX >= superView.frame.minX {
self.frame.size.width -= deltaX
self.frame.origin.x += deltaX
}
case .right:
if superView.frame.width / 3 ..< superView.frame.width ~= self.frame.size.width + deltaX,
self.frame.origin.x + self.frame.size.width + deltaX <= superView.frame.maxX {
self.frame.size.width += deltaX
}
case .none:
self.frame.origin.x += deltaX
self.frame.origin.y -= deltaY
}
self.repositionView()
}
@discardableResult
func cursorCornerBorderPosition(_ locationInView: CGPoint) -> CornerBorderPosition {
if locationInView.x < resizableArea,
locationInView.y < resizableArea {
return .bottomLeft
}
if self.bounds.width - locationInView.x < resizableArea,
locationInView.y < resizableArea {
return .bottomRight
}
if locationInView.x < resizableArea,
self.bounds.height - locationInView.y < resizableArea {
return .topLeft
}
if self.bounds.height - locationInView.y < resizableArea,
self.bounds.width - locationInView.x < resizableArea {
return .topRight
}
if locationInView.x < resizableArea {
return .left
}
if self.bounds.width - locationInView.x < resizableArea {
return .right
}
if locationInView.y < resizableArea {
return .bottom
}
if self.bounds.height - locationInView.y < resizableArea {
return .top
}
return .none
}
private func repositionView() {
guard let superView = superview else { return }
if self.frame.minX < superView.frame.minX {
self.frame.origin.x = superView.frame.minX
}
if self.frame.minY < superView.frame.minY {
self.frame.origin.y = superView.frame.minY
}
if self.frame.maxX > superView.frame.maxX {
self.frame.origin.x = superView.frame.maxX - self.frame.size.width
}
if self.frame.maxY > superView.frame.maxY {
self.frame.origin.y = superView.frame.maxY - self.frame.size.height
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | danielv |
Solution 2 | Akhil Shrivastav |
Solution 3 | ShaoJen Chen |