//
// DieView.swift
// Dice
//
// Created by Adam Preble on 8/22/14.
// Copyright © 2018 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank 11/20/18
import Cocoa
class DieView: NSView, NSDraggingSource
{
var intValue: Int? = 1
{
didSet
{
needsDisplay = true
}
}
var pressed: Bool = false
{
didSet
{
needsDisplay = true
}
}
override var intrinsicContentSize: NSSize
{
return NSSize(width: 20, height: 20)
}
var highlightForDragging: Bool = false
{
didSet
{
needsDisplay = true
}
}
var color: NSColor = NSColor.white
{
didSet
{
needsDisplay = true
}
}
var numberOfTimesToRoll: Int = 10
var mouseDownEvent: NSEvent?
var rollsRemaining: Int = 0
override init(frame frameRect: NSRect)
{
super.init(frame: frameRect)
commonInit()
}
required init?(coder decoder: NSCoder)
{
super.init(coder: decoder)
commonInit()
}
func commonInit()
{
self.registerForDraggedTypes([NSPasteboard.PasteboardType.string])
}
override func draw(_ dirtyRect: NSRect)
{
// Draw a lighGrey background
let backgroundColor = NSColor.lightGray
backgroundColor.set()
NSBezierPath.fill(bounds)
if highlightForDragging
{
let gradient = NSGradient(starting: color, ending: backgroundColor)
gradient!.draw(in: bounds, relativeCenterPosition: NSZeroPoint)
}
else
{
drawDieWithSize(bounds.size)
}
}
func metricsForSize(_ size: CGSize) -> (edgeLength: CGFloat, dieFrame: CGRect)
{
let edgeLength = min(size.width, size.height)
let padding = edgeLength/10.0
let drawingBounds = CGRect(x: 0, y: 0, width: edgeLength, height: edgeLength)
// let dieFrame = drawingBounds.insetBy(dx: padding, dy: padding)
var dieFrame = drawingBounds.insetBy(dx: padding, dy: padding)
if pressed
{
dieFrame = dieFrame.offsetBy(dx: 0, dy: -edgeLength/40)
}
return (edgeLength, dieFrame)
}
func drawDieWithSize(_ size: CGSize)
{
if let intValue = intValue
{
let (edgeLength, dieFrame) = metricsForSize(size)
let cornerRadius: CGFloat = edgeLength/5.0
let dotRadius = edgeLength/12.0
let dotFrame = dieFrame.insetBy(dx: dotRadius * 2.5, dy: dotRadius * 2.5)
// Push a copy of the state onto the stack before the shadow is set. Then pop that state before the dots are drawn.
NSGraphicsContext.saveGraphicsState()
let shadow = NSShadow()
shadow.shadowOffset = NSSize(width: 0, height: -1)
shadow.shadowBlurRadius = (pressed ? edgeLength/100 : edgeLength/20)
shadow.set()
// Draw the rounded shape of the die profile:
color.set()
NSBezierPath(roundedRect: dieFrame, xRadius: cornerRadius, yRadius: cornerRadius).fill()
NSGraphicsContext.restoreGraphicsState()
// Shadow will not apply to subsequent drawing commands
// Ready to draw the dots.
// The dots will be black:
NSColor.black.set()
// Nested function to make drawing dots cleaner:
func drawDot(_ u: CGFloat, _ v: CGFloat)
{
let dotOrigin = CGPoint(x: dotFrame.minX + dotFrame.width * u, y: dotFrame.minY + dotFrame.height * v)
let dotRect = CGRect(origin: dotOrigin, size: CGSize.zero).insetBy(dx: -dotRadius, dy: -dotRadius)
NSBezierPath(ovalIn: dotRect).fill()
}
// if intValue is in range...
if (1...6).index(of: intValue) != nil
{
// Draw the dots:
if [1, 3, 5].index(of: intValue) != nil
{
drawDot(0.5, 0.5) // Center dot
}
if (2...6).index(of: intValue) != nil
{
drawDot(0, 1) // Upper left
drawDot(1, 0) // Lower, right
}
if (4...6).index(of: intValue) != nil
{
drawDot(1, 1) // Upper right
drawDot(0, 0) // Lower left
}
if intValue == 6
{
drawDot(0, 0.5) // Mid left/right
drawDot(1, 0.5)
}
}
else
{
let paraStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
paraStyle.alignment = .center
let font = NSFont.systemFont(ofSize: edgeLength * 0.5)
let attrs = [NSAttributedString.Key.foregroundColor: NSColor.black,
NSAttributedString.Key.font: font,
NSAttributedString.Key.paragraphStyle: paraStyle]
let string = "\(intValue)" as NSString
string.drawCenteredInRect(dieFrame, attributes: attrs)
}
}
}
// MARK: - Mouse Events
override func mouseDown(with event: NSEvent)
{
print("mouseDown")
mouseDownEvent = event
let dieFrame = metricsForSize(bounds.size).dieFrame
let pointInView = convert(event.locationInWindow, from: nil)
pressed = dieFrame.contains(pointInView) // pressed is true only if clicked within the dieFrame view
}
override func mouseDragged(with event: NSEvent)
{
print("mouseDragged location: \(event.locationInWindow)")
let downPoint = mouseDownEvent!.locationInWindow
let dragPoint = event.locationInWindow
let distanceDragged = hypot(downPoint.x - dragPoint.x, downPoint.y - dragPoint.y)
if distanceDragged < 3
{
return
}
pressed = false
if let intValue = intValue
{
let imageSize = bounds.size
let image = NSImage(size: imageSize, flipped: false) { (imageBounds) in
self.drawDieWithSize(imageBounds.size)
return true
}
let draggingFrameOrigin = convert(downPoint, to: nil)
let draggingFrame = NSRect(origin: draggingFrameOrigin, size: imageSize).offsetBy(dx: -imageSize.width/2, dy: -imageSize.height/2)
let item = NSDraggingItem(pasteboardWriter: "\(intValue)" as NSPasteboardWriting)
item.draggingFrame = draggingFrame
item.imageComponentsProvider = { let component = NSDraggingImageComponent(key: NSDraggingItem.ImageComponentKey.icon)
component.contents = image
component.frame = NSRect(origin: NSPoint(), size: imageSize)
return [component]
}
beginDraggingSession(with: [item], event: mouseDownEvent!, source: self)
}
}
override func mouseUp(with event: NSEvent)
{
print("mouseUp click Count: \(event.clickCount)")
if event.clickCount == 2
{
roll()
}
pressed = false
}
func randomize()
{
intValue = Int(arc4random_uniform(5)) + 1
}
// MARK: - Rolling the Die (NSTimer)
func roll()
{
rollsRemaining = numberOfTimesToRoll
Timer.scheduledTimer(timeInterval: 0.15, target: self, selector: #selector(DieView.rollTick(_:)), userInfo: nil, repeats: true)
window?.makeFirstResponder(nil)
}
@objc func rollTick(_ sender: Timer)
{
let lastIntValue = intValue
while intValue == lastIntValue
{
randomize()
}
rollsRemaining -= 1
if rollsRemaining == 0
{
sender.invalidate()
window?.makeFirstResponder(self)
}
}
// MARK: - First Responder
override var acceptsFirstResponder: Bool { return true }
override func becomeFirstResponder() -> Bool
{
return true
}
override func resignFirstResponder() -> Bool
{
return true
}
override func drawFocusRingMask()
{
NSBezierPath.fill(bounds)
}
override var focusRingMaskBounds: NSRect
{
return bounds
}
// MARK: - Keyboard Events
override func keyDown(with event: NSEvent)
{
interpretKeyEvents([event])
}
override func insertText(_ insertString: Any)
{
let text = insertString as! String
if let number = Int(text)
{
intValue = number // number of dots in die will change based on (number 1-6) key pressed
}
}
override func insertTab(_ sender: Any?)
{
window?.selectNextKeyView(sender)
}
override func insertBacktab(_ sender: Any?)
{
window?.selectPreviousKeyView(sender)
}
// MARK: - Save as PDF
// NB: Under 'Target' > 'Dice', you have to enable 'App Sandbox' > 'File Access' > 'User Selected Access' > "Read/Write" (Permissions and Access)
// ... otherwise, app will crash when try to call NSSavePane()
@IBAction func savePDF(sender: AnyObject!)
{
let savePanel = NSSavePanel()
savePanel.allowedFileTypes = ["pdf"]
savePanel.beginSheetModal(for: window!,
completionHandler: {[unowned savePanel] (result) in
print("completionHandler")
if result == NSApplication.ModalResponse.OK
{
let data = self.dataWithPDF(inside: self.bounds)
do
{
try data.write(to: savePanel.url!,
options: NSData.WritingOptions.atomic)
}
catch let error as NSError
{
let alert = NSAlert(error: error)
alert.runModal()
}
catch
{
fatalError("unknown error")
}
}
})
}
// MARK: - Pasteboard
func writeToPasteboard(pasteboard: NSPasteboard)
{
if let intValue = intValue
{
pasteboard.clearContents()
pasteboard.writeObjects(["\(intValue)" as NSPasteboardWriting])
}
}
func readFromPasteboard(pasteboard: NSPasteboard) -> Bool
{
let objects = pasteboard.readObjects(forClasses: [NSString.self], options: [:]) as! [String]
if let str = objects.first
{
intValue = Int(str)
return true
}
return false
}
// NB: you need to use the "_" parameter name prefix, in order to match the default firstResponder method name (creating the connection automatically)
@IBAction func cut(_ sender: AnyObject?)
{
writeToPasteboard(pasteboard: NSPasteboard.general)
intValue = nil
}
@IBAction func copy(_ sender: AnyObject?)
{
writeToPasteboard(pasteboard: NSPasteboard.general)
}
@IBAction func paste(_ sender: AnyObject?)
{
_ = readFromPasteboard(pasteboard: NSPasteboard.general)
}
// MARK: - Drag Source
func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation
{
return [.copy, .delete]
}
func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation)
{
if operation == .delete
{
intValue = nil
}
}
// MARK: - Drag Destination
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation
{
if sender.draggingSource as! NSDraggingSource === self
{
return []
}
highlightForDragging = true
return sender.draggingSourceOperationMask
}
override func draggingExited(_ sender: NSDraggingInfo?)
{
highlightForDragging = false
}
override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool
{
return true
}
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool
{
let ok = readFromPasteboard(pasteboard: sender.draggingPasteboard)
return ok
}
override func concludeDragOperation(_ sender: NSDraggingInfo?)
{
highlightForDragging = false
}
}
//
// NSString+Drawing.swift
// Dice
//
// Created by Sesame Street on 11/17/18.
// Copyright © 2018 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank 11/20/18
import Cocoa
extension NSString
{
func drawCenteredInRect(_ rect: NSRect, attributes: [NSAttributedString.Key: Any]?)
{
let stringSize = size(withAttributes: attributes)
let point = NSPoint(x: rect.origin.x + (rect.width - stringSize.width)/2.0,
y: rect.origin.y + (rect.height - stringSize.height)/2.0)
draw(at: point, withAttributes: attributes)
}
}
//
// ConfigurationWindowController.swift
// Dice
//
// Created by Adam Preble on 8/22/14.
// Copyright © 2018 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank 11/20/18
import Cocoa
struct DieConfiguration
{
let color: NSColor
let rolls: Int
init(color: NSColor, rolls: Int)
{
self.color = color
self.rolls = max(rolls, 1)
}
}
class ConfigurationWindowController: NSWindowController
{
var configuration: DieConfiguration
{
set
{
color = newValue.color
rolls = newValue.rolls
}
get
{
return DieConfiguration(color: color, rolls: rolls)
}
}
@objc private dynamic var color: NSColor = NSColor.white
@objc private dynamic var rolls: Int = 10
override var windowNibName: NSNib.Name?
{
return "ConfigurationWindowController"
}
@IBAction func okayButtonClicked(_ button: NSButton)
{
print("OK clicked")
window?.endEditing(for: nil)
dismissWithModalResponse(NSApplication.ModalResponse.OK)
}
@IBAction func cancelButtonClicked(_ button: NSButton)
{
print("Cancel clicked")
dismissWithModalResponse(NSApplication.ModalResponse.cancel)
}
func dismissWithModalResponse(_ response: NSApplication.ModalResponse)
{
window!.sheetParent!.endSheet(window!, returnCode: response)
}
}
//
// AppDelegate.swift
// Dice
//
// Created by Adam Preble on 8/22/14.
// Copyright © 2018 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank 11/20/18
import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate
{
var mainWindowController: MainWindowController?
func applicationDidFinishLaunching(_ aNotification: Notification)
{
// Create a window controller
let mainWindowController = MainWindowController()
// Put the window of the window controller on screen
mainWindowController.showWindow(self)
// Set the property to point to the window controller
self.mainWindowController = mainWindowController
}
}
//
// MainWindowController.swift
// Dice
//
// Created by Adam Preble on 8/22/14.
// Copyright © 2018 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank 11/20/18
import Cocoa
class MainWindowController: NSWindowController
{
override var windowNibName: NSNib.Name?
{
return "MainWindowController"
}
override func windowDidLoad()
{
super.windowDidLoad()
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
}
// MARK: - Actions
var configurationWindowController: ConfigurationWindowController?
@IBAction func showDieConfiguration(_ sender: AnyObject?)
{
print("Configuration menu item clicked")
if let window = window, let dieView = window.firstResponder as? DieView
{
// Create and configure the window controller to present as a sheet:
let windowController = ConfigurationWindowController()
windowController.configuration = DieConfiguration(color: dieView.color, rolls: dieView.numberOfTimesToRoll)
window.beginSheet(windowController.window!, completionHandler: { response in
// The sheet has finished. Did the user click 'OK'?
if response == NSApplication.ModalResponse.OK
{
let configuration = self.configurationWindowController!.configuration
dieView.color = configuration.color
dieView.numberOfTimesToRoll = configuration.rolls
}
// All done with the window controller.
self.configurationWindowController = nil
})
configurationWindowController = windowController
}
}
}