2016-01-04 23:40:43 -05:00
|
|
|
//
|
|
|
|
// MachineDocument.swift
|
|
|
|
// Clock Signal
|
|
|
|
//
|
|
|
|
// Created by Thomas Harte on 04/01/2016.
|
2018-05-13 15:19:52 -04:00
|
|
|
// Copyright 2016 Thomas Harte. All rights reserved.
|
2016-01-04 23:40:43 -05:00
|
|
|
//
|
|
|
|
|
2016-01-14 20:33:22 -05:00
|
|
|
import AudioToolbox
|
2018-07-27 23:37:24 -04:00
|
|
|
import Cocoa
|
2016-01-04 23:40:43 -05:00
|
|
|
|
2016-06-16 20:51:35 -04:00
|
|
|
class MachineDocument:
|
|
|
|
NSDocument,
|
|
|
|
NSWindowDelegate,
|
2018-03-22 09:48:19 -04:00
|
|
|
CSMachineDelegate,
|
2016-06-16 20:51:35 -04:00
|
|
|
CSOpenGLViewDelegate,
|
|
|
|
CSOpenGLViewResponderDelegate,
|
|
|
|
CSBestEffortUpdaterDelegate,
|
|
|
|
CSAudioQueueDelegate
|
|
|
|
{
|
2018-02-18 22:09:03 -05:00
|
|
|
fileprivate let actionLock = NSLock()
|
|
|
|
fileprivate let drawLock = NSLock()
|
|
|
|
fileprivate let bestEffortLock = NSLock()
|
|
|
|
|
2016-10-02 16:57:57 -04:00
|
|
|
var machine: CSMachine!
|
2016-06-27 21:38:14 -04:00
|
|
|
var name: String! {
|
|
|
|
get {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
2018-04-02 23:31:36 -04:00
|
|
|
var optionsPanelNibName: String?
|
2016-05-31 22:32:38 -04:00
|
|
|
|
2016-06-01 19:04:07 -04:00
|
|
|
func aspectRatio() -> NSSize {
|
|
|
|
return NSSize(width: 4.0, height: 3.0)
|
|
|
|
}
|
|
|
|
|
2017-02-11 12:58:47 -05:00
|
|
|
@IBOutlet weak var openGLView: CSOpenGLView!
|
2016-10-02 16:31:50 -04:00
|
|
|
@IBOutlet var optionsPanel: MachinePanel!
|
2016-09-15 22:12:12 -04:00
|
|
|
@IBAction func showOptions(_ sender: AnyObject!) {
|
2016-04-17 21:43:39 -04:00
|
|
|
optionsPanel?.setIsVisible(true)
|
|
|
|
}
|
|
|
|
|
2018-06-17 22:52:17 -04:00
|
|
|
@IBOutlet var activityPanel: NSPanel!
|
|
|
|
@IBAction func showActivity(_ sender: AnyObject!) {
|
|
|
|
activityPanel.setIsVisible(true)
|
|
|
|
}
|
|
|
|
|
2016-09-15 22:12:12 -04:00
|
|
|
fileprivate var audioQueue: CSAudioQueue! = nil
|
2017-09-30 21:34:43 -04:00
|
|
|
fileprivate var bestEffortUpdater: CSBestEffortUpdater?
|
2016-01-14 20:33:22 -05:00
|
|
|
|
2017-09-19 23:06:37 -04:00
|
|
|
override var windowNibName: NSNib.Name? {
|
2019-03-26 22:15:38 -04:00
|
|
|
return "MachineDocument"
|
2016-10-02 16:57:57 -04:00
|
|
|
}
|
|
|
|
|
2016-09-15 22:12:12 -04:00
|
|
|
override func windowControllerDidLoadNib(_ aController: NSWindowController) {
|
2016-01-04 23:40:43 -05:00
|
|
|
super.windowControllerDidLoadNib(aController)
|
2018-04-02 22:42:41 -04:00
|
|
|
aController.window?.contentAspectRatio = self.aspectRatio()
|
|
|
|
setupMachineOutput()
|
2018-04-03 18:47:07 -04: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
|
2019-03-26 22:15:38 -04:00
|
|
|
Bundle.main.loadNibNamed("MachinePicker", owner: self, topLevelObjects: nil)
|
2018-04-03 23:01:12 -04:00
|
|
|
self.machinePicker?.establishStoredOptions()
|
2018-04-03 18:47:07 -04:00
|
|
|
self.windowControllers[0].window?.beginSheet(self.machinePickerPanel!, completionHandler: nil)
|
2018-04-02 23:31:36 -04:00
|
|
|
}
|
2018-04-02 22:42:41 -04:00
|
|
|
}
|
2016-01-04 23:40:43 -05:00
|
|
|
|
2018-04-02 22:42:41 -04:00
|
|
|
fileprivate func setupMachineOutput() {
|
|
|
|
if let machine = self.machine, let openGLView = self.openGLView {
|
2019-06-11 18:21:56 -04:00
|
|
|
// Establish the output aspect ratio and audio.
|
2018-04-02 22:42:41 -04:00
|
|
|
let aspectRatio = self.aspectRatio()
|
|
|
|
openGLView.perform(glContext: {
|
|
|
|
machine.setView(openGLView, aspectRatio: Float(aspectRatio.width / aspectRatio.height))
|
|
|
|
})
|
2016-05-31 22:36:53 -04:00
|
|
|
|
2019-06-11 18:21:56 -04:00
|
|
|
// Attach an options panel if one is available.
|
2018-04-02 23:31:36 -04:00
|
|
|
if let optionsPanelNibName = self.optionsPanelNibName {
|
2019-03-26 22:15:38 -04:00
|
|
|
Bundle.main.loadNibNamed(optionsPanelNibName, owner: self, topLevelObjects: nil)
|
2018-04-02 23:31:36 -04:00
|
|
|
self.optionsPanel.machine = machine
|
|
|
|
self.optionsPanel?.establishStoredOptions()
|
|
|
|
showOptions(self)
|
|
|
|
}
|
|
|
|
|
2018-04-02 22:42:41 -04:00
|
|
|
machine.delegate = self
|
|
|
|
self.bestEffortUpdater = CSBestEffortUpdater()
|
2017-02-11 12:58:47 -05:00
|
|
|
|
2019-06-11 18:21:56 -04: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.
|
2018-04-02 22:42:41 -04:00
|
|
|
openGLView.delegate = self
|
|
|
|
openGLView.responderDelegate = self
|
2017-02-11 12:58:47 -05:00
|
|
|
|
2019-06-11 18:21:56 -04:00
|
|
|
// If this machine has a mouse, enable mouse capture.
|
|
|
|
openGLView.shouldCaptureMouse = machine.hasMouse
|
|
|
|
|
2018-04-02 22:42:41 -04:00
|
|
|
setupAudioQueueClockRate()
|
2016-10-24 22:08:24 -04:00
|
|
|
|
2019-06-11 18:21:56 -04:00
|
|
|
// Bring OpenGL view-holding window on top of the options panel and show the content.
|
2018-04-03 18:47:07 -04:00
|
|
|
openGLView.isHidden = false
|
2018-04-03 22:22:39 -04:00
|
|
|
openGLView.window!.makeKeyAndOrderFront(self)
|
|
|
|
openGLView.window!.makeFirstResponder(openGLView)
|
2017-02-26 21:58:43 -05:00
|
|
|
|
2019-06-11 18:21:56 -04:00
|
|
|
// Start accepting best effort updates.
|
2018-04-02 22:42:41 -04:00
|
|
|
self.bestEffortUpdater!.delegate = self
|
|
|
|
}
|
2016-06-20 21:47:27 -04:00
|
|
|
}
|
|
|
|
|
2018-06-17 18:53:56 -04:00
|
|
|
func machineSpeakerDidChangeInputClock(_ machine: CSMachine) {
|
2018-03-22 09:49:36 -04:00
|
|
|
setupAudioQueueClockRate()
|
2018-03-22 09:48:19 -04:00
|
|
|
}
|
|
|
|
|
2018-03-22 09:49:36 -04:00
|
|
|
fileprivate func setupAudioQueueClockRate() {
|
2016-06-01 19:04:07 -04:00
|
|
|
// establish and provide the audio queue, taking advice as to an appropriate sampling rate
|
2016-06-15 08:07:25 -04:00
|
|
|
let maximumSamplingRate = CSAudioQueue.preferredSamplingRate()
|
2016-09-15 22:12:12 -04:00
|
|
|
let selectedSamplingRate = self.machine.idealSamplingRate(from: NSRange(location: 0, length: NSInteger(maximumSamplingRate)))
|
2016-06-05 11:20:05 -04:00
|
|
|
if selectedSamplingRate > 0 {
|
2016-06-15 08:07:25 -04:00
|
|
|
audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate))
|
2016-06-16 20:51:35 -04:00
|
|
|
audioQueue.delegate = self
|
2016-06-23 21:09:34 -04:00
|
|
|
self.machine.audioQueue = self.audioQueue
|
2016-10-10 07:45:09 -04:00
|
|
|
self.machine.setAudioSamplingRate(selectedSamplingRate, bufferSize:audioQueue.preferredBufferSize)
|
2016-06-05 11:20:05 -04:00
|
|
|
}
|
2016-01-04 23:40:43 -05:00
|
|
|
}
|
|
|
|
|
2016-05-31 22:32:38 -04:00
|
|
|
override func close() {
|
2018-06-17 22:52:17 -04:00
|
|
|
activityPanel?.setIsVisible(false)
|
|
|
|
activityPanel = nil
|
|
|
|
|
2017-09-30 20:07:04 -04:00
|
|
|
optionsPanel?.setIsVisible(false)
|
|
|
|
optionsPanel = nil
|
|
|
|
|
2018-02-18 22:09:03 -05:00
|
|
|
bestEffortLock.lock()
|
2018-04-02 22:42:41 -04:00
|
|
|
if let bestEffortUpdater = bestEffortUpdater {
|
|
|
|
bestEffortUpdater.delegate = nil
|
|
|
|
bestEffortUpdater.flush()
|
|
|
|
self.bestEffortUpdater = nil
|
|
|
|
}
|
2018-02-18 22:09:03 -05:00
|
|
|
bestEffortLock.unlock()
|
2017-09-30 20:07:04 -04:00
|
|
|
|
2016-05-31 22:32:38 -04:00
|
|
|
actionLock.lock()
|
|
|
|
drawLock.lock()
|
2017-09-30 20:07:04 -04:00
|
|
|
machine = nil
|
2018-02-18 22:09:03 -05:00
|
|
|
openGLView.delegate = nil
|
2016-05-31 22:32:38 -04:00
|
|
|
openGLView.invalidate()
|
|
|
|
actionLock.unlock()
|
|
|
|
drawLock.unlock()
|
|
|
|
|
|
|
|
super.close()
|
|
|
|
}
|
|
|
|
|
2016-08-31 22:03:42 -04:00
|
|
|
// MARK: configuring
|
2016-09-15 22:12:12 -04:00
|
|
|
func configureAs(_ analysis: CSStaticAnalyser) {
|
2018-01-24 20:14:15 -05:00
|
|
|
if let machine = CSMachine(analyser: analysis) {
|
2016-10-02 16:57:57 -04:00
|
|
|
self.machine = machine
|
2018-04-02 23:31:36 -04:00
|
|
|
self.optionsPanelNibName = analysis.optionsPanelNibName
|
2018-04-02 22:42:41 -04:00
|
|
|
setupMachineOutput()
|
2018-06-17 18:53:56 -04:00
|
|
|
setupActivityDisplay()
|
2018-02-12 21:46:21 -05:00
|
|
|
}
|
2016-08-31 22:03:42 -04:00
|
|
|
}
|
|
|
|
|
2018-04-03 18:47:07 -04:00
|
|
|
fileprivate var shouldShowNewMachinePanel = false
|
2016-10-02 17:04:14 -04: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-11 21:03:01 -04:00
|
|
|
} else {
|
|
|
|
throw NSError(domain: "MachineDocument", code: -1, userInfo: nil)
|
2016-10-02 17:04:14 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-02 23:31:36 -04:00
|
|
|
convenience init(type typeName: String) throws {
|
|
|
|
self.init()
|
|
|
|
self.fileType = typeName
|
2018-04-03 18:47:07 -04:00
|
|
|
self.shouldShowNewMachinePanel = true
|
2018-04-02 23:31:36 -04:00
|
|
|
}
|
|
|
|
|
2016-06-19 16:35:04 -04:00
|
|
|
// MARK: the pasteboard
|
2018-05-13 11:12:03 -04:00
|
|
|
func paste(_ sender: Any) {
|
2017-09-19 23:06:37 -04:00
|
|
|
let pasteboard = NSPasteboard.general
|
|
|
|
if let string = pasteboard.string(forType: .string) {
|
2016-06-23 21:09:34 -04:00
|
|
|
self.machine.paste(string)
|
2016-06-19 16:35:04 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-16 20:51:35 -04:00
|
|
|
// MARK: CSBestEffortUpdaterDelegate
|
2018-03-21 22:18:13 -04:00
|
|
|
final func bestEffortUpdater(_ bestEffortUpdater: CSBestEffortUpdater!, runForInterval duration: TimeInterval, didSkipPreviousUpdate: Bool) {
|
|
|
|
if actionLock.try() {
|
|
|
|
self.machine.run(forInterval: duration)
|
|
|
|
actionLock.unlock()
|
2016-06-16 20:51:35 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: CSAudioQueueDelegate
|
2016-10-17 08:18:32 -04:00
|
|
|
final func audioQueueIsRunningDry(_ audioQueue: CSAudioQueue) {
|
2018-02-18 22:09:03 -05:00
|
|
|
bestEffortLock.lock()
|
2017-09-30 21:34:43 -04:00
|
|
|
bestEffortUpdater?.update()
|
2018-02-18 22:09:03 -05:00
|
|
|
bestEffortLock.unlock()
|
2016-05-31 22:32:38 -04:00
|
|
|
}
|
|
|
|
|
2016-06-16 20:51:35 -04:00
|
|
|
// MARK: CSOpenGLViewDelegate
|
2019-03-02 19:33:28 -05:00
|
|
|
final func openGLViewRedraw(_ view: CSOpenGLView, event redrawEvent: CSOpenGLViewRedrawEvent) {
|
2019-03-02 23:17:31 -05:00
|
|
|
if redrawEvent == .timer {
|
|
|
|
bestEffortLock.lock()
|
|
|
|
if let bestEffortUpdater = bestEffortUpdater {
|
|
|
|
bestEffortLock.unlock()
|
|
|
|
bestEffortUpdater.update()
|
|
|
|
} else {
|
|
|
|
bestEffortLock.unlock()
|
2019-03-02 21:27:34 -05:00
|
|
|
}
|
2016-05-31 22:32:38 -04:00
|
|
|
}
|
2019-03-02 23:17:31 -05:00
|
|
|
|
|
|
|
if drawLock.try() {
|
|
|
|
if redrawEvent == .timer {
|
|
|
|
machine.updateView(forPixelSize: view.backingSize)
|
|
|
|
}
|
|
|
|
machine.drawView(forPixelSize: view.backingSize)
|
|
|
|
drawLock.unlock()
|
|
|
|
}
|
2016-05-31 22:32:38 -04:00
|
|
|
}
|
2016-01-04 23:40:43 -05:00
|
|
|
|
2018-08-04 22:21:23 -04:00
|
|
|
// MARK: Runtime media insertion.
|
2017-08-17 10:48:29 -04:00
|
|
|
final func openGLView(_ view: CSOpenGLView, didReceiveFileAt URL: URL) {
|
2017-08-17 11:00:08 -04:00
|
|
|
let mediaSet = CSMediaSet(fileAt: URL)
|
|
|
|
if let mediaSet = mediaSet {
|
|
|
|
mediaSet.apply(to: self.machine)
|
|
|
|
}
|
2017-08-17 10:48:29 -04:00
|
|
|
}
|
|
|
|
|
2018-08-04 22:21:23 -04:00
|
|
|
@IBAction final func insertMedia(_ sender: AnyObject!) {
|
2018-08-05 13:12:02 -04:00
|
|
|
let openPanel = NSOpenPanel()
|
2018-08-06 18:56:59 -04:00
|
|
|
openPanel.message = "Hint: you can also insert media by dragging and dropping it onto the machine's window."
|
2018-08-05 13:12:02 -04:00
|
|
|
openPanel.beginSheetModal(for: self.windowControllers[0].window!) { (response) in
|
2018-08-04 22:21:23 -04:00
|
|
|
if response == .OK {
|
2018-08-05 13:12:02 -04:00
|
|
|
for url in openPanel.urls {
|
|
|
|
let mediaSet = CSMediaSet(fileAt: url)
|
|
|
|
if let mediaSet = mediaSet {
|
|
|
|
mediaSet.apply(to: self.machine)
|
|
|
|
}
|
|
|
|
}
|
2018-08-04 22:21:23 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-04 21:43:50 -04:00
|
|
|
// MARK: NSDocument overrides
|
2016-09-15 22:12:12 -04:00
|
|
|
override func data(ofType typeName: String) throws -> Data {
|
2016-06-04 21:43:50 -04:00
|
|
|
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
|
|
|
|
}
|
2016-06-05 08:53:05 -04:00
|
|
|
|
2016-10-03 08:01:04 -04:00
|
|
|
// MARK: Input management
|
2016-09-15 22:12:12 -04:00
|
|
|
func windowDidResignKey(_ notification: Notification) {
|
2018-04-02 23:31:36 -04:00
|
|
|
if let machine = self.machine {
|
|
|
|
machine.clearAllKeys()
|
2018-07-22 16:55:47 -04:00
|
|
|
machine.joystickManager = nil
|
|
|
|
}
|
2019-06-11 16:35:04 -04:00
|
|
|
self.openGLView.releaseMouse()
|
2018-07-22 16:55:47 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func windowDidBecomeKey(_ notification: Notification) {
|
|
|
|
if let machine = self.machine {
|
|
|
|
machine.joystickManager = (DocumentController.shared as! DocumentController).joystickManager
|
2018-04-02 23:31:36 -04:00
|
|
|
}
|
2016-06-05 08:53:05 -04:00
|
|
|
}
|
|
|
|
|
2016-09-15 22:12:12 -04:00
|
|
|
func keyDown(_ event: NSEvent) {
|
2018-04-02 23:31:36 -04:00
|
|
|
if let machine = self.machine {
|
|
|
|
machine.setKey(event.keyCode, characters: event.characters, isPressed: true)
|
|
|
|
}
|
2016-06-05 08:53:05 -04:00
|
|
|
}
|
|
|
|
|
2016-09-15 22:12:12 -04:00
|
|
|
func keyUp(_ event: NSEvent) {
|
2018-04-02 23:31:36 -04:00
|
|
|
if let machine = self.machine {
|
|
|
|
machine.setKey(event.keyCode, characters: event.characters, isPressed: false)
|
|
|
|
}
|
2016-06-05 08:53:05 -04:00
|
|
|
}
|
|
|
|
|
2016-09-15 22:12:12 -04:00
|
|
|
func flagsChanged(_ newModifiers: NSEvent) {
|
2018-04-02 23:31:36 -04: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))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-11 16:35:04 -04:00
|
|
|
func mouseMoved(_ event: NSEvent) {
|
2019-06-11 18:41:41 -04:00
|
|
|
if let machine = self.machine {
|
|
|
|
machine.addMouseMotionX(event.deltaX, y: event.deltaY)
|
|
|
|
}
|
2019-06-11 16:35:04 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func mouseUp(_ event: NSEvent) {
|
2019-06-11 18:41:41 -04:00
|
|
|
if let machine = self.machine {
|
|
|
|
machine.setMouseButton(Int32(event.buttonNumber), isPressed: false)
|
|
|
|
}
|
2019-06-11 16:35:04 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func mouseDown(_ event: NSEvent) {
|
2019-06-11 18:41:41 -04:00
|
|
|
if let machine = self.machine {
|
|
|
|
machine.setMouseButton(Int32(event.buttonNumber), isPressed: true)
|
|
|
|
}
|
2019-06-11 16:35:04 -04:00
|
|
|
}
|
|
|
|
|
2018-04-02 23:31:36 -04:00
|
|
|
// MARK: New machine creation
|
|
|
|
@IBOutlet var machinePicker: MachinePicker?
|
2018-04-03 18:47:07 -04:00
|
|
|
@IBOutlet var machinePickerPanel: NSWindow?
|
2018-04-02 23:31:36 -04:00
|
|
|
@IBAction func createMachine(_ sender: NSButton?) {
|
|
|
|
self.configureAs(machinePicker!.selectedMachine())
|
|
|
|
machinePicker = nil
|
2018-08-05 22:47:51 -04:00
|
|
|
self.windowControllers[0].window?.endSheet(self.machinePickerPanel!)
|
2018-04-02 23:31:36 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func cancelCreateMachine(_ sender: NSButton?) {
|
|
|
|
close()
|
2016-06-05 08:53:05 -04:00
|
|
|
}
|
2018-06-13 19:22:34 -04:00
|
|
|
|
|
|
|
// MARK: Joystick-via-the-keyboard selection
|
|
|
|
@IBAction func useKeyboardAsKeyboard(_ sender: NSMenuItem?) {
|
|
|
|
machine.inputMode = .keyboard
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func useKeyboardAsJoystick(_ sender: NSMenuItem?) {
|
|
|
|
machine.inputMode = .joystick
|
|
|
|
}
|
|
|
|
|
|
|
|
override func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
|
|
|
if let menuItem = item as? NSMenuItem {
|
|
|
|
switch item.action {
|
|
|
|
case #selector(self.useKeyboardAsKeyboard):
|
2018-10-24 21:59:30 -04:00
|
|
|
if machine == nil || !machine.hasExclusiveKeyboard {
|
2018-08-06 18:52:42 -04:00
|
|
|
menuItem.state = .off
|
2018-06-13 19:22:34 -04:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
menuItem.state = machine.inputMode == .keyboard ? .on : .off
|
|
|
|
return true
|
|
|
|
|
|
|
|
case #selector(self.useKeyboardAsJoystick):
|
|
|
|
if machine == nil || !machine.hasJoystick {
|
2018-08-06 18:52:42 -04:00
|
|
|
menuItem.state = .off
|
2018-06-13 19:22:34 -04:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
menuItem.state = machine.inputMode == .joystick ? .on : .off
|
|
|
|
return true
|
|
|
|
|
2018-06-17 22:52:17 -04:00
|
|
|
case #selector(self.showActivity(_:)):
|
|
|
|
return self.activityPanel != nil
|
|
|
|
|
2018-08-06 18:52:42 -04:00
|
|
|
case #selector(self.insertMedia(_:)):
|
|
|
|
return self.machine != nil && self.machine.canInsertMedia
|
|
|
|
|
2018-06-13 19:22:34 -04:00
|
|
|
default: break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return super.validateUserInterfaceItem(item)
|
|
|
|
}
|
2018-06-17 18:53:56 -04:00
|
|
|
|
2018-07-27 23:37:24 -04:00
|
|
|
// Screenshot capture.
|
|
|
|
@IBAction func saveScreenshot(_ sender: AnyObject!) {
|
|
|
|
// Grab a date formatter and form a file name.
|
|
|
|
let dateFormatter = DateFormatter()
|
|
|
|
dateFormatter.dateStyle = .short
|
|
|
|
dateFormatter.timeStyle = .long
|
|
|
|
|
|
|
|
let filename = ("Clock Signal Screen Shot " + dateFormatter.string(from: Date()) + ".png").replacingOccurrences(of: "/", with: "-")
|
|
|
|
.replacingOccurrences(of: ":", with: ".")
|
|
|
|
let pictursURL = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask)[0]
|
|
|
|
let url = pictursURL.appendingPathComponent(filename)
|
|
|
|
|
|
|
|
// Obtain the machine's current display.
|
|
|
|
var imageRepresentation: NSBitmapImageRep? = nil
|
|
|
|
self.openGLView.perform {
|
|
|
|
imageRepresentation = self.machine.imageRepresentation
|
|
|
|
}
|
|
|
|
|
|
|
|
// Encode as a PNG and save.
|
|
|
|
let pngData = imageRepresentation!.representation(using: .png, properties: [:])
|
|
|
|
try! pngData?.write(to: url)
|
|
|
|
}
|
|
|
|
|
2018-06-17 18:53:56 -04:00
|
|
|
// MARK: Activity display.
|
2018-06-18 21:22:51 -04:00
|
|
|
class LED {
|
|
|
|
let levelIndicator: NSLevelIndicator
|
|
|
|
init(levelIndicator: NSLevelIndicator) {
|
|
|
|
self.levelIndicator = levelIndicator
|
|
|
|
}
|
|
|
|
var isLit = false
|
|
|
|
var isBlinking = false
|
|
|
|
}
|
|
|
|
fileprivate var leds: [String: LED] = [:]
|
2018-06-17 18:53:56 -04:00
|
|
|
func setupActivityDisplay() {
|
2018-06-17 22:52:17 -04:00
|
|
|
var leds = machine.leds
|
|
|
|
if leds.count > 0 {
|
2019-03-26 22:15:38 -04:00
|
|
|
Bundle.main.loadNibNamed("Activity", owner: self, topLevelObjects: nil)
|
2018-06-17 22:52:17 -04:00
|
|
|
showActivity(nil)
|
|
|
|
|
|
|
|
// Inspect the activity panel for indicators.
|
|
|
|
var activityIndicators: [NSLevelIndicator] = []
|
|
|
|
var textFields: [NSTextField] = []
|
|
|
|
if let contentView = self.activityPanel.contentView {
|
|
|
|
for view in contentView.subviews {
|
|
|
|
if let levelIndicator = view as? NSLevelIndicator {
|
|
|
|
activityIndicators.append(levelIndicator)
|
|
|
|
}
|
|
|
|
|
|
|
|
if let textField = view as? NSTextField {
|
|
|
|
textFields.append(textField)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there are fewer level indicators than LEDs, trim that list.
|
|
|
|
if activityIndicators.count < leds.count {
|
|
|
|
leds.removeSubrange(activityIndicators.count ..< leds.count)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove unused views.
|
|
|
|
for c in leds.count ..< activityIndicators.count {
|
|
|
|
textFields[c].removeFromSuperview()
|
|
|
|
activityIndicators[c].removeFromSuperview()
|
|
|
|
}
|
|
|
|
|
2018-06-18 21:22:51 -04:00
|
|
|
// Apply labels and create leds entries.
|
2018-06-17 22:52:17 -04:00
|
|
|
for c in 0 ..< leds.count {
|
|
|
|
textFields[c].stringValue = leds[c]
|
2018-06-18 21:22:51 -04:00
|
|
|
self.leds[leds[c]] = LED(levelIndicator: activityIndicators[c])
|
|
|
|
}
|
2018-06-23 19:44:35 -04:00
|
|
|
|
|
|
|
// Add a constraints to minimise window height.
|
|
|
|
let heightConstraint = NSLayoutConstraint(
|
|
|
|
item: self.activityPanel.contentView!,
|
|
|
|
attribute: .bottom,
|
|
|
|
relatedBy: .equal,
|
|
|
|
toItem: activityIndicators[leds.count-1],
|
|
|
|
attribute: .bottom,
|
|
|
|
multiplier: 1.0,
|
|
|
|
constant: 20.0)
|
|
|
|
self.activityPanel.contentView?.addConstraint(heightConstraint)
|
2018-06-18 21:22:51 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func machine(_ machine: CSMachine, ledShouldBlink ledName: String) {
|
|
|
|
// If there is such an LED, switch it off for 0.03 of a second; if it's meant
|
|
|
|
// to be off at the end of that, leave it off. Don't allow the blinks to
|
|
|
|
// pile up — allow there to be only one in flight at a time.
|
|
|
|
if let led = leds[ledName] {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
if !led.isBlinking {
|
|
|
|
led.levelIndicator.floatValue = 0.0
|
|
|
|
led.isBlinking = true
|
|
|
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.03) {
|
|
|
|
led.levelIndicator.floatValue = led.isLit ? 1.0 : 0.0
|
|
|
|
led.isBlinking = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func machine(_ machine: CSMachine, led ledName: String, didChangeToLit isLit: Bool) {
|
|
|
|
// If there is such an LED, switch it appropriately.
|
|
|
|
if let led = leds[ledName] {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
led.levelIndicator.floatValue = isLit ? 1.0 : 0.0
|
|
|
|
led.isLit = isLit
|
2018-06-17 18:53:56 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-01-04 23:40:43 -05:00
|
|
|
}
|