2016-01-05 04:40:43 +00:00
|
|
|
//
|
|
|
|
// MachineDocument.swift
|
|
|
|
// Clock Signal
|
|
|
|
//
|
|
|
|
// Created by Thomas Harte on 04/01/2016.
|
2018-05-13 19:19:52 +00:00
|
|
|
// Copyright 2016 Thomas Harte. All rights reserved.
|
2016-01-05 04:40:43 +00:00
|
|
|
//
|
|
|
|
|
|
|
|
import Cocoa
|
2016-01-15 01:33:22 +00:00
|
|
|
import AudioToolbox
|
2016-01-05 04:40:43 +00:00
|
|
|
|
2016-06-17 00:51:35 +00:00
|
|
|
class MachineDocument:
|
|
|
|
NSDocument,
|
|
|
|
NSWindowDelegate,
|
2018-03-22 13:48:19 +00:00
|
|
|
CSMachineDelegate,
|
2016-06-17 00:51:35 +00:00
|
|
|
CSOpenGLViewDelegate,
|
|
|
|
CSOpenGLViewResponderDelegate,
|
|
|
|
CSBestEffortUpdaterDelegate,
|
|
|
|
CSAudioQueueDelegate
|
|
|
|
{
|
2018-02-19 03:09:03 +00:00
|
|
|
fileprivate let actionLock = NSLock()
|
|
|
|
fileprivate let drawLock = NSLock()
|
|
|
|
fileprivate let bestEffortLock = NSLock()
|
|
|
|
|
2016-10-02 20:57:57 +00:00
|
|
|
var machine: CSMachine!
|
2016-06-28 01:38:14 +00:00
|
|
|
var name: String! {
|
|
|
|
get {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
2018-04-03 03:31:36 +00:00
|
|
|
var optionsPanelNibName: String?
|
2016-06-01 02:32:38 +00:00
|
|
|
|
2016-06-01 23:04:07 +00:00
|
|
|
func aspectRatio() -> NSSize {
|
|
|
|
return NSSize(width: 4.0, height: 3.0)
|
|
|
|
}
|
|
|
|
|
2017-02-11 17:58:47 +00:00
|
|
|
@IBOutlet weak var openGLView: CSOpenGLView!
|
2016-10-02 20:31:50 +00:00
|
|
|
@IBOutlet var optionsPanel: MachinePanel!
|
2016-09-16 02:12:12 +00:00
|
|
|
@IBAction func showOptions(_ sender: AnyObject!) {
|
2016-04-18 01:43:39 +00:00
|
|
|
optionsPanel?.setIsVisible(true)
|
|
|
|
}
|
|
|
|
|
2016-09-16 02:12:12 +00:00
|
|
|
fileprivate var audioQueue: CSAudioQueue! = nil
|
2017-10-01 01:34:43 +00:00
|
|
|
fileprivate var bestEffortUpdater: CSBestEffortUpdater?
|
2016-01-15 01:33:22 +00:00
|
|
|
|
2017-09-20 03:06:37 +00:00
|
|
|
override var windowNibName: NSNib.Name? {
|
|
|
|
return NSNib.Name(rawValue: "MachineDocument")
|
2016-10-02 20:57:57 +00:00
|
|
|
}
|
|
|
|
|
2016-09-16 02:12:12 +00:00
|
|
|
override func windowControllerDidLoadNib(_ aController: NSWindowController) {
|
2016-01-05 04:40:43 +00:00
|
|
|
super.windowControllerDidLoadNib(aController)
|
2018-04-03 02:42:41 +00:00
|
|
|
aController.window?.contentAspectRatio = self.aspectRatio()
|
|
|
|
setupMachineOutput()
|
2018-04-03 03:31:36 +00:00
|
|
|
|
2018-04-03 22:47:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Attempting to show a sheet before the window is visible (such as when the NIB is loaded) results in
|
|
|
|
// a sheet mysteriously floating on its own. For now, use windowDidUpdate as a proxy to know that the window
|
|
|
|
// is visible, though it's a little premature.
|
|
|
|
func windowDidUpdate(_ notification: Notification) {
|
|
|
|
if self.shouldShowNewMachinePanel {
|
|
|
|
self.shouldShowNewMachinePanel = false
|
2018-04-03 03:31:36 +00:00
|
|
|
Bundle.main.loadNibNamed(NSNib.Name(rawValue: "MachinePicker"), owner: self, topLevelObjects: nil)
|
2018-04-04 03:01:12 +00:00
|
|
|
self.machinePicker?.establishStoredOptions()
|
2018-04-03 22:47:07 +00:00
|
|
|
self.windowControllers[0].window?.beginSheet(self.machinePickerPanel!, completionHandler: nil)
|
2018-04-03 03:31:36 +00:00
|
|
|
}
|
2018-04-03 02:42:41 +00:00
|
|
|
}
|
2016-01-05 04:40:43 +00:00
|
|
|
|
2018-04-03 02:42:41 +00:00
|
|
|
fileprivate func setupMachineOutput() {
|
|
|
|
if let machine = self.machine, let openGLView = self.openGLView {
|
|
|
|
// establish the output aspect ratio and audio
|
|
|
|
let aspectRatio = self.aspectRatio()
|
|
|
|
openGLView.perform(glContext: {
|
|
|
|
machine.setView(openGLView, aspectRatio: Float(aspectRatio.width / aspectRatio.height))
|
|
|
|
})
|
2016-06-01 02:36:53 +00:00
|
|
|
|
2018-04-03 03:31:36 +00:00
|
|
|
// attach an options panel if one is available
|
|
|
|
if let optionsPanelNibName = self.optionsPanelNibName {
|
|
|
|
Bundle.main.loadNibNamed(NSNib.Name(rawValue: optionsPanelNibName), owner: self, topLevelObjects: nil)
|
|
|
|
self.optionsPanel.machine = machine
|
|
|
|
self.optionsPanel?.establishStoredOptions()
|
|
|
|
showOptions(self)
|
|
|
|
}
|
|
|
|
|
2018-04-03 02:42:41 +00:00
|
|
|
machine.delegate = self
|
|
|
|
self.bestEffortUpdater = CSBestEffortUpdater()
|
2017-02-11 17:58:47 +00:00
|
|
|
|
2018-04-03 02:42:41 +00:00
|
|
|
// callbacks from the OpenGL may come on a different thread, immediately following the .delegate set;
|
|
|
|
// hence the full setup of the best-effort updater prior to setting self as a delegate
|
|
|
|
openGLView.delegate = self
|
|
|
|
openGLView.responderDelegate = self
|
2017-02-11 17:58:47 +00:00
|
|
|
|
2018-04-03 02:42:41 +00:00
|
|
|
setupAudioQueueClockRate()
|
2016-10-25 02:08:24 +00:00
|
|
|
|
2018-04-03 22:47:07 +00:00
|
|
|
// bring OpenGL view-holding window on top of the options panel and show the content
|
|
|
|
openGLView.isHidden = false
|
2018-04-04 02:22:39 +00:00
|
|
|
openGLView.window!.makeKeyAndOrderFront(self)
|
|
|
|
openGLView.window!.makeFirstResponder(openGLView)
|
2017-02-27 02:58:43 +00:00
|
|
|
|
2018-04-03 02:42:41 +00:00
|
|
|
// start accepting best effort updates
|
|
|
|
self.bestEffortUpdater!.delegate = self
|
|
|
|
}
|
2016-06-21 01:47:27 +00:00
|
|
|
}
|
|
|
|
|
2018-03-22 13:48:19 +00:00
|
|
|
func machineSpeakerDidChangeInputClock(_ machine: CSMachine!) {
|
2018-03-22 13:49:36 +00:00
|
|
|
setupAudioQueueClockRate()
|
2018-03-22 13:48:19 +00:00
|
|
|
}
|
|
|
|
|
2018-03-22 13:49:36 +00:00
|
|
|
fileprivate func setupAudioQueueClockRate() {
|
2016-06-01 23:04:07 +00:00
|
|
|
// establish and provide the audio queue, taking advice as to an appropriate sampling rate
|
2016-06-15 12:07:25 +00:00
|
|
|
let maximumSamplingRate = CSAudioQueue.preferredSamplingRate()
|
2016-09-16 02:12:12 +00:00
|
|
|
let selectedSamplingRate = self.machine.idealSamplingRate(from: NSRange(location: 0, length: NSInteger(maximumSamplingRate)))
|
2016-06-05 15:20:05 +00:00
|
|
|
if selectedSamplingRate > 0 {
|
2016-06-15 12:07:25 +00:00
|
|
|
audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate))
|
2016-06-17 00:51:35 +00:00
|
|
|
audioQueue.delegate = self
|
2016-06-24 01:09:34 +00:00
|
|
|
self.machine.audioQueue = self.audioQueue
|
2016-10-10 11:45:09 +00:00
|
|
|
self.machine.setAudioSamplingRate(selectedSamplingRate, bufferSize:audioQueue.preferredBufferSize)
|
2016-06-05 15:20:05 +00:00
|
|
|
}
|
2016-01-05 04:40:43 +00:00
|
|
|
}
|
|
|
|
|
2016-06-01 02:32:38 +00:00
|
|
|
override func close() {
|
2017-10-01 00:07:04 +00:00
|
|
|
optionsPanel?.setIsVisible(false)
|
|
|
|
optionsPanel = nil
|
|
|
|
|
2018-02-19 03:09:03 +00:00
|
|
|
bestEffortLock.lock()
|
2018-04-03 02:42:41 +00:00
|
|
|
if let bestEffortUpdater = bestEffortUpdater {
|
|
|
|
bestEffortUpdater.delegate = nil
|
|
|
|
bestEffortUpdater.flush()
|
|
|
|
self.bestEffortUpdater = nil
|
|
|
|
}
|
2018-02-19 03:09:03 +00:00
|
|
|
bestEffortLock.unlock()
|
2017-10-01 00:07:04 +00:00
|
|
|
|
2016-06-01 02:32:38 +00:00
|
|
|
actionLock.lock()
|
|
|
|
drawLock.lock()
|
2017-10-01 00:07:04 +00:00
|
|
|
machine = nil
|
2018-02-19 03:09:03 +00:00
|
|
|
openGLView.delegate = nil
|
2016-06-01 02:32:38 +00:00
|
|
|
openGLView.invalidate()
|
|
|
|
actionLock.unlock()
|
|
|
|
drawLock.unlock()
|
|
|
|
|
|
|
|
super.close()
|
|
|
|
}
|
|
|
|
|
2016-09-01 02:03:42 +00:00
|
|
|
// MARK: configuring
|
2016-09-16 02:12:12 +00:00
|
|
|
func configureAs(_ analysis: CSStaticAnalyser) {
|
2018-01-25 01:14:15 +00:00
|
|
|
if let machine = CSMachine(analyser: analysis) {
|
2016-10-02 20:57:57 +00:00
|
|
|
self.machine = machine
|
2018-04-03 03:31:36 +00:00
|
|
|
self.optionsPanelNibName = analysis.optionsPanelNibName
|
2018-04-03 02:42:41 +00:00
|
|
|
setupMachineOutput()
|
2018-02-13 02:46:21 +00:00
|
|
|
}
|
2016-09-01 02:03:42 +00:00
|
|
|
}
|
|
|
|
|
2018-04-03 22:47:07 +00:00
|
|
|
fileprivate var shouldShowNewMachinePanel = false
|
2016-10-02 21:04:14 +00:00
|
|
|
override func read(from url: URL, ofType typeName: String) throws {
|
|
|
|
if let analyser = CSStaticAnalyser(fileAt: url) {
|
|
|
|
self.displayName = analyser.displayName
|
|
|
|
self.configureAs(analyser)
|
2016-10-12 01:03:01 +00:00
|
|
|
} else {
|
|
|
|
throw NSError(domain: "MachineDocument", code: -1, userInfo: nil)
|
2016-10-02 21:04:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-03 03:31:36 +00:00
|
|
|
convenience init(type typeName: String) throws {
|
|
|
|
self.init()
|
|
|
|
self.fileType = typeName
|
2018-04-03 22:47:07 +00:00
|
|
|
self.shouldShowNewMachinePanel = true
|
2018-04-03 03:31:36 +00:00
|
|
|
}
|
|
|
|
|
2016-06-19 20:35:04 +00:00
|
|
|
// MARK: the pasteboard
|
2018-05-13 15:12:03 +00:00
|
|
|
func paste(_ sender: Any) {
|
2017-09-20 03:06:37 +00:00
|
|
|
let pasteboard = NSPasteboard.general
|
|
|
|
if let string = pasteboard.string(forType: .string) {
|
2016-06-24 01:09:34 +00:00
|
|
|
self.machine.paste(string)
|
2016-06-19 20:35:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-17 00:51:35 +00:00
|
|
|
// MARK: CSBestEffortUpdaterDelegate
|
2018-03-22 02:18:13 +00:00
|
|
|
final func bestEffortUpdater(_ bestEffortUpdater: CSBestEffortUpdater!, runForInterval duration: TimeInterval, didSkipPreviousUpdate: Bool) {
|
|
|
|
if actionLock.try() {
|
|
|
|
self.machine.run(forInterval: duration)
|
|
|
|
actionLock.unlock()
|
2016-06-17 00:51:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: CSAudioQueueDelegate
|
2016-10-17 12:18:32 +00:00
|
|
|
final func audioQueueIsRunningDry(_ audioQueue: CSAudioQueue) {
|
2018-02-19 03:09:03 +00:00
|
|
|
bestEffortLock.lock()
|
2017-10-01 01:34:43 +00:00
|
|
|
bestEffortUpdater?.update()
|
2018-02-19 03:09:03 +00:00
|
|
|
bestEffortLock.unlock()
|
2016-06-01 02:32:38 +00:00
|
|
|
}
|
|
|
|
|
2016-06-17 00:51:35 +00:00
|
|
|
// MARK: CSOpenGLViewDelegate
|
2016-09-16 02:12:12 +00:00
|
|
|
final func openGLView(_ view: CSOpenGLView, drawViewOnlyIfDirty onlyIfDirty: Bool) {
|
2018-02-19 03:09:03 +00:00
|
|
|
bestEffortLock.lock()
|
2017-10-01 01:34:43 +00:00
|
|
|
if let bestEffortUpdater = bestEffortUpdater {
|
2018-02-19 23:44:12 +00:00
|
|
|
bestEffortLock.unlock()
|
2017-10-01 01:34:43 +00:00
|
|
|
bestEffortUpdater.update()
|
|
|
|
if drawLock.try() {
|
|
|
|
self.machine.drawView(forPixelSize: view.backingSize, onlyIfDirty: onlyIfDirty)
|
|
|
|
drawLock.unlock()
|
|
|
|
}
|
2018-02-19 23:44:12 +00:00
|
|
|
} else {
|
|
|
|
bestEffortLock.unlock()
|
2016-06-01 02:32:38 +00:00
|
|
|
}
|
|
|
|
}
|
2016-01-05 04:40:43 +00:00
|
|
|
|
2017-08-17 14:48:29 +00:00
|
|
|
final func openGLView(_ view: CSOpenGLView, didReceiveFileAt URL: URL) {
|
2017-08-17 15:00:08 +00:00
|
|
|
let mediaSet = CSMediaSet(fileAt: URL)
|
|
|
|
if let mediaSet = mediaSet {
|
|
|
|
mediaSet.apply(to: self.machine)
|
|
|
|
}
|
2017-08-17 14:48:29 +00:00
|
|
|
}
|
|
|
|
|
2016-06-05 01:43:50 +00:00
|
|
|
// MARK: NSDocument overrides
|
2016-09-16 02:12:12 +00:00
|
|
|
override func data(ofType typeName: String) throws -> Data {
|
2016-06-05 01:43:50 +00:00
|
|
|
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
|
|
|
|
}
|
2016-06-05 12:53:05 +00:00
|
|
|
|
2016-10-03 12:01:04 +00:00
|
|
|
// MARK: Input management
|
2016-09-16 02:12:12 +00:00
|
|
|
func windowDidResignKey(_ notification: Notification) {
|
2018-04-03 03:31:36 +00:00
|
|
|
if let machine = self.machine {
|
|
|
|
machine.clearAllKeys()
|
|
|
|
}
|
2016-06-05 12:53:05 +00:00
|
|
|
}
|
|
|
|
|
2016-09-16 02:12:12 +00:00
|
|
|
func keyDown(_ event: NSEvent) {
|
2018-04-03 03:31:36 +00:00
|
|
|
if let machine = self.machine {
|
|
|
|
machine.setKey(event.keyCode, characters: event.characters, isPressed: true)
|
|
|
|
}
|
2016-06-05 12:53:05 +00:00
|
|
|
}
|
|
|
|
|
2016-09-16 02:12:12 +00:00
|
|
|
func keyUp(_ event: NSEvent) {
|
2018-04-03 03:31:36 +00:00
|
|
|
if let machine = self.machine {
|
|
|
|
machine.setKey(event.keyCode, characters: event.characters, isPressed: false)
|
|
|
|
}
|
2016-06-05 12:53:05 +00:00
|
|
|
}
|
|
|
|
|
2016-09-16 02:12:12 +00:00
|
|
|
func flagsChanged(_ newModifiers: NSEvent) {
|
2018-04-03 03:31:36 +00:00
|
|
|
if let machine = self.machine {
|
|
|
|
machine.setKey(VK_Shift, characters: nil, isPressed: newModifiers.modifierFlags.contains(.shift))
|
|
|
|
machine.setKey(VK_Control, characters: nil, isPressed: newModifiers.modifierFlags.contains(.control))
|
|
|
|
machine.setKey(VK_Command, characters: nil, isPressed: newModifiers.modifierFlags.contains(.command))
|
|
|
|
machine.setKey(VK_Option, characters: nil, isPressed: newModifiers.modifierFlags.contains(.option))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: New machine creation
|
|
|
|
@IBOutlet var machinePicker: MachinePicker?
|
2018-04-03 22:47:07 +00:00
|
|
|
@IBOutlet var machinePickerPanel: NSWindow?
|
2018-04-03 03:31:36 +00:00
|
|
|
@IBAction func createMachine(_ sender: NSButton?) {
|
|
|
|
self.configureAs(machinePicker!.selectedMachine())
|
|
|
|
machinePicker = nil
|
|
|
|
sender?.window?.close()
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func cancelCreateMachine(_ sender: NSButton?) {
|
|
|
|
close()
|
2016-06-05 12:53:05 +00:00
|
|
|
}
|
2016-01-05 04:40:43 +00:00
|
|
|
}
|