Sheets: DieView.swift and associated files for Swift 4.2 and Xcode 10.1


#1
//
//  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
        }
    }
    
}

#2

Great. Ill check icloud back with you after running this through and gimp tell if theres any error on the code.

Regards,
Brian.