Swift/AlignmentGuides/AlignmentGuidesView.swift
/* |
Copyright (C) 2017 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
`AlignmentGuidesView` shows how to use `NSAlignmentFeedbackFilter` to align a box to the edges of its container and the container's center. |
The sample shows how to feed the filter using both a tracking loop (see `mouseDown()`) and a gesture recognizer (see `panGestureUpdated()`). These two methods feed the filter the latest event information, and then call into the appropriate handler for mouse down/dragged/up. |
*/ |
import Cocoa |
class AlignmentGuidesView: DrawingView, NSGestureRecognizerDelegate { |
// MARK: Properties |
@IBOutlet weak var eventHandlingButton: NSPopUpButton! |
@IBOutlet weak var useGridButton: NSButton! |
let feedbackFilter = NSAlignmentFeedbackFilter() |
var boxFrame = NSRect(x: 180, y: 210, width: 200, height: 200) |
var dragOriginOffset = NSPoint.zero |
var drawCenterXAsHoldingItem = false |
var drawCenterYAsHoldingItem = false |
// MARK: Initialisers |
required init?(coder: NSCoder) { |
super.init(coder: coder) |
} |
/** |
Updating the filter using a tracking loop. mouseDown tracks the entire mouse |
drag, updating the alignment filter with each event. |
*/ |
override func mouseDown(with event: NSEvent) { |
// If we aren't using the tracking loop, bail here. |
guard eventHandlingButton.indexOfSelectedItem == 0 else { return } |
let point = convert(event.locationInWindow, from: nil) |
guard handleMouseDown(point) else { return } |
feedbackFilter.update(with: event) |
let mask: NSEventMask = [NSAlignmentFeedbackFilter.inputEventMask(), .leftMouseUp, .leftMouseDragged] |
window!.trackEvents(matching: mask, timeout: NSEventDurationForever, mode: RunLoopMode.eventTrackingRunLoopMode) { event, stop in |
self.feedbackFilter.update(with: event) |
// Store the last frame shown to the user |
let fromFrame = self.boxFrame |
// Handle different event types |
switch event.type { |
case .leftMouseUp: |
self.handleMouseUp() |
stop.pointee = true |
case .leftMouseDragged: |
let point = self.convert(event.locationInWindow, from: nil) |
self.handleMouseDragged(point) |
default: () |
} |
/* |
Now perform alignment. It's important to do this for every event |
so that periodic events can cause alignment to happen (consider |
case of fast-moving cursor stopping over a guide). |
*/ |
self.boxFrame = self.performAlignment(fromFrame, draggedTo: self.boxFrame) |
self.setNeedsDisplay(self.bounds) |
} |
} |
/** |
Updating the filter using a pan gesture recognizer. `panGestureUpdated` |
updates the alignment filter for each movement. |
*/ |
@objc func gestureRecognizerShouldBegin(_ recognizer: NSGestureRecognizer) -> Bool { |
/* |
If we aren't using the gesture recogniaed, bail here since `mouseDown()` |
will handle it. |
*/ |
guard eventHandlingButton.indexOfSelectedItem == 1 else { return false } |
let locationInView = recognizer.location(in: self) |
// Return whether or not a draggable object was clicked. |
return handleMouseDown(locationInView) |
} |
@IBAction func magnificationChanged(_ slider: NSSlider) { |
enclosingScrollView!.magnification = CGFloat(slider.doubleValue) |
} |
@IBAction func panGestureUpdated(_ recognizer: NSPanGestureRecognizer) { |
// Store the last frame shown to the user |
let fromFrame = boxFrame |
switch recognizer.state { |
case .began: |
feedbackFilter.update(withPanRecognizer: recognizer) |
case .changed: |
feedbackFilter.update(withPanRecognizer: recognizer) |
handleMouseDragged(recognizer.location(in: self)) |
case .ended, .cancelled: |
handleMouseUp() |
default: () |
} |
/* |
Now perform alignment. It's important to do this for every action so |
that periodic events can cause alignment to happen (consider case |
of fast-moving cursor stopping over a guide). |
*/ |
boxFrame = performAlignment(fromFrame, draggedTo:boxFrame) |
setNeedsDisplay(bounds) |
} |
// MARK: Mouse Down / Dragged / Up Handling. |
/* |
On drag update where the item would be without any alignment (its offset), |
and then call into `performAlignment`. |
*/ |
/// Generic handler for starting drag. Returns whether or not a draggable object was clicked |
func handleMouseDown(_ location: NSPoint) -> Bool { |
dragOriginOffset = NSPoint(x: location.x - boxFrame.origin.x, y: location.y - boxFrame.origin.y) |
if !NSPointInRect(location, boxFrame) { |
return false |
} |
// Start a drag. |
return true |
} |
/// Generic handler for moving a drag |
func handleMouseDragged(_ location: NSPoint) { |
boxFrame.origin = NSPoint(x: location.x - dragOriginOffset.x, y: location.y - dragOriginOffset.y) |
boxFrame = constrainRectToBounds(boxFrame) |
if useGridButton.state == NSOnState { |
boxFrame.origin.x = round(boxFrame.origin.x / 10.0) * 10.0 |
boxFrame.origin.y = round(boxFrame.origin.y / 10.0) * 10.0 |
} |
let visibleRect = NSRect(x: location.x, y: location.y, width: 1.0, height: 1.0) |
scrollToVisible(visibleRect) |
} |
/// Generic handler for stopping a drag. |
func handleMouseUp() { } |
func performAlignment(_ movedFrom: NSRect, draggedTo: NSRect) -> NSRect { |
var newRect = draggedTo |
// Align the edges of the box to the edges of the container. |
var preparedAlignments = [NSAlignmentFeedbackToken]() |
if let token = feedbackFilter.alignmentFeedbackTokenForHorizontalMovement(in: self, previousX: movedFrom.minX, alignedX: 0.0, defaultX: draggedTo.minX) { |
newRect.origin.x = 0.0 |
preparedAlignments += [token] |
} |
if let token = feedbackFilter.alignmentFeedbackTokenForHorizontalMovement(in: self, previousX: movedFrom.maxX, alignedX: bounds.width, defaultX: draggedTo.maxX) { |
newRect.origin.x = bounds.width - newRect.width |
preparedAlignments += [token] |
} |
if let token = feedbackFilter.alignmentFeedbackTokenForVerticalMovement(in: self, previousY: movedFrom.minY, alignedY: 0.0, defaultY: draggedTo.minY) { |
newRect.origin.y = 0.0 |
preparedAlignments += [token] |
} |
if let token = feedbackFilter.alignmentFeedbackTokenForVerticalMovement(in: self, previousY: movedFrom.maxY, alignedY: bounds.height, defaultY: draggedTo.maxY) { |
newRect.origin.y = bounds.height - newRect.height |
preparedAlignments += [token] |
} |
// Align the centers of the box to the centers of the container. |
if let token = feedbackFilter.alignmentFeedbackTokenForHorizontalMovement(in: self, previousX: movedFrom.midX, alignedX: bounds.midX, defaultX: draggedTo.midX) { |
newRect.origin.x = bounds.midX - newRect.width / 2.0 |
drawCenterXAsHoldingItem = true |
preparedAlignments += [token] |
} |
else { |
drawCenterXAsHoldingItem = false |
} |
if let token = feedbackFilter.alignmentFeedbackTokenForVerticalMovement(in: self, previousY: movedFrom.midY, alignedY: bounds.midY, defaultY: draggedTo.midY) { |
newRect.origin.y = bounds.midY - newRect.size.height / 2.0 |
drawCenterYAsHoldingItem = true |
preparedAlignments += [token] |
} |
else { |
drawCenterYAsHoldingItem = false |
} |
feedbackFilter.performFeedback(preparedAlignments, performanceTime: .drawCompleted) |
return newRect |
} |
override func draw(_ dirtyRect: NSRect) { |
/* |
Your app can draw however it wants. The box could be a subview, or |
anything really. Here we're drawing using bezier paths. |
*/ |
super.draw(dirtyRect) |
drawHorizontalGuide(bounds.midY, holdingItem: drawCenterYAsHoldingItem) |
drawVerticalGuide(bounds.midX, holdingItem: drawCenterXAsHoldingItem) |
drawBox(boxFrame, drawCenterAlignmentGuides: true) |
} |
} |
Copyright © 2017 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2017-09-19