mirror of
https://github.com/TomHarte/CLK.git
synced 2024-09-04 06:29:47 +00:00
Merge pull request #635 from TomHarte/Cleanup
Improves code quality, particularly the NSDocument subclass.
This commit is contained in:
commit
0c8e313fd5
@ -63,6 +63,7 @@ uint8_t IWM::read(int address) {
|
|||||||
|
|
||||||
if(data_register_ & 0x80) {
|
if(data_register_ & 0x80) {
|
||||||
// printf("\n\nIWM:%02x\n\n", data_register_);
|
// printf("\n\nIWM:%02x\n\n", data_register_);
|
||||||
|
// printf(".");
|
||||||
data_register_ = 0;
|
data_register_ = 0;
|
||||||
}
|
}
|
||||||
// LOG("Reading data register: " << PADHEX(2) << int(result));
|
// LOG("Reading data register: " << PADHEX(2) << int(result));
|
||||||
|
@ -23,26 +23,37 @@ void DriveSpeedAccumulator::post_sample(uint8_t sample) {
|
|||||||
if(sample_pointer_ == samples_.size()) {
|
if(sample_pointer_ == samples_.size()) {
|
||||||
sample_pointer_ = 0;
|
sample_pointer_ = 0;
|
||||||
|
|
||||||
// Treat 33 as a zero point and count zero crossings; then approximate
|
// The below fits for a function like `a + bc`; it encapsultes the following
|
||||||
// the RPM from the frequency of those.
|
// beliefs:
|
||||||
int samples_over = 0;
|
//
|
||||||
int sum = 0;
|
// (i) motor speed is proportional to voltage supplied;
|
||||||
const uint8_t centre = 33;
|
// (ii) with pulse-width modulation it's therefore proportional to the duty cycle;
|
||||||
for(size_t c = 0; c < 512; ++c) {
|
// (iii) the Mac pulse-width modulates whatever it reads from the disk speed buffer;
|
||||||
if(samples_[c] > centre) ++ samples_over;
|
// (iv) ... subject to software pulse-width modulation of that pulse-width modulation.
|
||||||
sum += samples_[c];
|
//
|
||||||
}
|
// So, I believe current motor speed is proportional to a low-pass filtering of
|
||||||
|
// the speed buffer. Which I've implemented very coarsely via 'large' bucketed-averages,
|
||||||
|
// noting also that exact disk motor speed is always a little approximate.
|
||||||
|
|
||||||
|
// Sum all samples.
|
||||||
// TODO: if the above is the correct test, do it on sample receipt rather than
|
// TODO: if the above is the correct test, do it on sample receipt rather than
|
||||||
// bothering with an intermediate buffer.
|
// bothering with an intermediate buffer.
|
||||||
|
int sum = 0;
|
||||||
|
for(auto s: samples_) {
|
||||||
|
sum += s;
|
||||||
|
}
|
||||||
|
|
||||||
// The below fits for a function like `a + bc`.
|
// The formula below was derived from observing values the Mac wrote into its
|
||||||
const float rotation_speed = (float(sum) * 0.052896440564137f) - 259.0f;
|
// disk-speed buffer. Given that it runs a calibration loop before doing so,
|
||||||
|
// I cannot guarantee the accuracy of these numbers beyond being within the
|
||||||
|
// range that the computer would accept.
|
||||||
|
const float normalised_sum = float(sum) / float(samples_.size());
|
||||||
|
const float rotation_speed = (normalised_sum * 27.08f) - 259.0f;
|
||||||
|
|
||||||
for(int c = 0; c < number_of_drives_; ++c) {
|
for(int c = 0; c < number_of_drives_; ++c) {
|
||||||
drives_[c]->set_rotation_speed(rotation_speed);
|
drives_[c]->set_rotation_speed(rotation_speed);
|
||||||
}
|
}
|
||||||
// printf("RPM: %0.2f (%d over; %d sum)\n", rotation_speed, samples_over, sum);
|
// printf("RPM: %0.2f (%d sum)\n", rotation_speed, sum);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ class DriveSpeedAccumulator {
|
|||||||
void add_drive(Apple::Macintosh::DoubleDensityDrive *drive);
|
void add_drive(Apple::Macintosh::DoubleDensityDrive *drive);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::array<uint8_t, 512> samples_;
|
std::array<uint8_t, 20> samples_;
|
||||||
std::size_t sample_pointer_ = 0;
|
std::size_t sample_pointer_ = 0;
|
||||||
Apple::Macintosh::DoubleDensityDrive *drives_[2] = {nullptr, nullptr};
|
Apple::Macintosh::DoubleDensityDrive *drives_[2] = {nullptr, nullptr};
|
||||||
int number_of_drives_ = 0;
|
int number_of_drives_ = 0;
|
||||||
|
@ -19,68 +19,168 @@ class MachineDocument:
|
|||||||
CSAudioQueueDelegate,
|
CSAudioQueueDelegate,
|
||||||
CSROMReciverViewDelegate
|
CSROMReciverViewDelegate
|
||||||
{
|
{
|
||||||
fileprivate let actionLock = NSLock()
|
// MARK: - Mutual Exclusion.
|
||||||
fileprivate let drawLock = NSLock()
|
|
||||||
fileprivate let bestEffortLock = NSLock()
|
|
||||||
|
|
||||||
var machine: CSMachine!
|
/// Ensures exclusive access between calls to self.machine.run and close().
|
||||||
var name: String! {
|
private let actionLock = NSLock()
|
||||||
get {
|
/// Ensures exclusive access between calls to machine.updateView and machine.drawView, and close().
|
||||||
return nil
|
private let drawLock = NSLock()
|
||||||
}
|
/// Ensures exclusive access to the best-effort updater.
|
||||||
}
|
private let bestEffortLock = NSLock()
|
||||||
var optionsPanelNibName: String?
|
|
||||||
|
|
||||||
func aspectRatio() -> NSSize {
|
// MARK: - Machine details.
|
||||||
|
|
||||||
|
/// A description of the machine this document should represent once fully set up.
|
||||||
|
private var machineDescription: CSStaticAnalyser?
|
||||||
|
|
||||||
|
/// The active machine, following its successful creation.
|
||||||
|
private var machine: CSMachine!
|
||||||
|
|
||||||
|
/// @returns the appropriate window content aspect ratio for this @c self.machine.
|
||||||
|
private func aspectRatio() -> NSSize {
|
||||||
return NSSize(width: 4.0, height: 3.0)
|
return NSSize(width: 4.0, height: 3.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The output audio queue, if any.
|
||||||
|
private var audioQueue: CSAudioQueue!
|
||||||
|
|
||||||
|
/// The best-effort updater.
|
||||||
|
private var bestEffortUpdater: CSBestEffortUpdater?
|
||||||
|
|
||||||
|
// MARK: - Main NIB connections.
|
||||||
|
|
||||||
|
/// The OpenGL view to receive this machine's display.
|
||||||
@IBOutlet weak var openGLView: CSOpenGLView!
|
@IBOutlet weak var openGLView: CSOpenGLView!
|
||||||
|
|
||||||
|
/// The options panel, if any.
|
||||||
@IBOutlet var optionsPanel: MachinePanel!
|
@IBOutlet var optionsPanel: MachinePanel!
|
||||||
|
|
||||||
|
/// An action to display the options panel, if there is one.
|
||||||
@IBAction func showOptions(_ sender: AnyObject!) {
|
@IBAction func showOptions(_ sender: AnyObject!) {
|
||||||
optionsPanel?.setIsVisible(true)
|
optionsPanel?.setIsVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The activity panel, if one is deemed appropriate.
|
||||||
@IBOutlet var activityPanel: NSPanel!
|
@IBOutlet var activityPanel: NSPanel!
|
||||||
|
|
||||||
|
/// An action to display the activity panel, if there is one.
|
||||||
@IBAction func showActivity(_ sender: AnyObject!) {
|
@IBAction func showActivity(_ sender: AnyObject!) {
|
||||||
activityPanel.setIsVisible(true)
|
activityPanel.setIsVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate var audioQueue: CSAudioQueue! = nil
|
// MARK: - NSDocument Overrides and NSWindowDelegate methods.
|
||||||
fileprivate var bestEffortUpdater: CSBestEffortUpdater?
|
|
||||||
|
|
||||||
|
/// Links this class to the MachineDocument NIB.
|
||||||
override var windowNibName: NSNib.Name? {
|
override var windowNibName: NSNib.Name? {
|
||||||
return "MachineDocument"
|
return "MachineDocument"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
convenience init(type typeName: String) throws {
|
||||||
|
self.init()
|
||||||
|
self.fileType = typeName
|
||||||
|
}
|
||||||
|
|
||||||
|
override func read(from url: URL, ofType typeName: String) throws {
|
||||||
|
if let analyser = CSStaticAnalyser(fileAt: url) {
|
||||||
|
self.displayName = analyser.displayName
|
||||||
|
self.configureAs(analyser)
|
||||||
|
} else {
|
||||||
|
throw NSError(domain: "MachineDocument", code: -1, userInfo: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func close() {
|
||||||
|
activityPanel?.setIsVisible(false)
|
||||||
|
activityPanel = nil
|
||||||
|
|
||||||
|
optionsPanel?.setIsVisible(false)
|
||||||
|
optionsPanel = nil
|
||||||
|
|
||||||
|
bestEffortLock.lock()
|
||||||
|
if let bestEffortUpdater = bestEffortUpdater {
|
||||||
|
bestEffortUpdater.delegate = nil
|
||||||
|
bestEffortUpdater.flush()
|
||||||
|
self.bestEffortUpdater = nil
|
||||||
|
}
|
||||||
|
bestEffortLock.unlock()
|
||||||
|
|
||||||
|
actionLock.lock()
|
||||||
|
drawLock.lock()
|
||||||
|
machine = nil
|
||||||
|
openGLView.delegate = nil
|
||||||
|
openGLView.invalidate()
|
||||||
|
actionLock.unlock()
|
||||||
|
drawLock.unlock()
|
||||||
|
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func data(ofType typeName: String) throws -> Data {
|
||||||
|
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
|
||||||
|
}
|
||||||
|
|
||||||
override func windowControllerDidLoadNib(_ aController: NSWindowController) {
|
override func windowControllerDidLoadNib(_ aController: NSWindowController) {
|
||||||
super.windowControllerDidLoadNib(aController)
|
super.windowControllerDidLoadNib(aController)
|
||||||
aController.window?.contentAspectRatio = self.aspectRatio()
|
aController.window?.contentAspectRatio = self.aspectRatio()
|
||||||
if self.machine != nil {
|
}
|
||||||
|
|
||||||
|
private var missingROMs: [CSMissingROM] = []
|
||||||
|
func configureAs(_ analysis: CSStaticAnalyser) {
|
||||||
|
self.machineDescription = analysis
|
||||||
|
|
||||||
|
let missingROMs = NSMutableArray()
|
||||||
|
if let machine = CSMachine(analyser: analysis, missingROMs: missingROMs) {
|
||||||
|
self.machine = machine
|
||||||
setupMachineOutput()
|
setupMachineOutput()
|
||||||
|
setupActivityDisplay()
|
||||||
} else {
|
} else {
|
||||||
// This is somewhat of a desperate workaround; just having loaded the Nib doesn't
|
// Store the selected machine and list of missing ROMs, and
|
||||||
// mean that the window is visible yet, but presenting the ROM import sheet before
|
// show the missing ROMs dialogue.
|
||||||
// the window is visible will result in it being free floating.
|
self.missingROMs = []
|
||||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .milliseconds(500)) {
|
for untypedMissingROM in missingROMs {
|
||||||
self.configureAs(self.selectedMachine!)
|
self.missingROMs.append(untypedMissingROM as! CSMissingROM)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestRoms()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum InteractionMode {
|
||||||
|
case notStarted, showingMachinePicker, showingROMRequester, showingMachine
|
||||||
|
}
|
||||||
|
private var interactionMode: InteractionMode = .notStarted
|
||||||
|
|
||||||
// Attempting to show a sheet before the window is visible (such as when the NIB is loaded) results in
|
// 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
|
// 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.
|
// is visible, though it's a little premature.
|
||||||
func windowDidUpdate(_ notification: Notification) {
|
func windowDidUpdate(_ notification: Notification) {
|
||||||
if self.shouldShowNewMachinePanel {
|
// If an interaction mode is not yet in effect, pick the proper one and display the relevant thing.
|
||||||
self.shouldShowNewMachinePanel = false
|
if self.interactionMode == .notStarted {
|
||||||
|
// If a full machine exists, just continue showing it.
|
||||||
|
if self.machine != nil {
|
||||||
|
self.interactionMode = .showingMachine
|
||||||
|
setupMachineOutput()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a machine has been picked but is not showing, there must be ROMs missing.
|
||||||
|
if self.machineDescription != nil {
|
||||||
|
self.interactionMode = .showingROMRequester
|
||||||
|
requestRoms()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a machine hasn't even been picked yet, show the machine picker.
|
||||||
|
self.interactionMode = .showingMachinePicker
|
||||||
Bundle.main.loadNibNamed("MachinePicker", owner: self, topLevelObjects: nil)
|
Bundle.main.loadNibNamed("MachinePicker", owner: self, topLevelObjects: nil)
|
||||||
self.machinePicker?.establishStoredOptions()
|
self.machinePicker?.establishStoredOptions()
|
||||||
self.windowControllers[0].window?.beginSheet(self.machinePickerPanel!, completionHandler: nil)
|
self.windowControllers[0].window?.beginSheet(self.machinePickerPanel!, completionHandler: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func setupMachineOutput() {
|
// MARK: - Connections Between Machine and the Outside World
|
||||||
|
|
||||||
|
private func setupMachineOutput() {
|
||||||
if let machine = self.machine, let openGLView = self.openGLView {
|
if let machine = self.machine, let openGLView = self.openGLView {
|
||||||
// Establish the output aspect ratio and audio.
|
// Establish the output aspect ratio and audio.
|
||||||
let aspectRatio = self.aspectRatio()
|
let aspectRatio = self.aspectRatio()
|
||||||
@ -89,7 +189,7 @@ class MachineDocument:
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Attach an options panel if one is available.
|
// Attach an options panel if one is available.
|
||||||
if let optionsPanelNibName = self.optionsPanelNibName {
|
if let optionsPanelNibName = self.machineDescription?.optionsPanelNibName {
|
||||||
Bundle.main.loadNibNamed(optionsPanelNibName, owner: self, topLevelObjects: nil)
|
Bundle.main.loadNibNamed(optionsPanelNibName, owner: self, topLevelObjects: nil)
|
||||||
self.optionsPanel.machine = machine
|
self.optionsPanel.machine = machine
|
||||||
self.optionsPanel?.establishStoredOptions()
|
self.optionsPanel?.establishStoredOptions()
|
||||||
@ -123,7 +223,7 @@ class MachineDocument:
|
|||||||
setupAudioQueueClockRate()
|
setupAudioQueueClockRate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func setupAudioQueueClockRate() {
|
private func setupAudioQueueClockRate() {
|
||||||
// establish and provide the audio queue, taking advice as to an appropriate sampling rate
|
// establish and provide the audio queue, taking advice as to an appropriate sampling rate
|
||||||
let maximumSamplingRate = CSAudioQueue.preferredSamplingRate()
|
let maximumSamplingRate = CSAudioQueue.preferredSamplingRate()
|
||||||
let selectedSamplingRate = self.machine.idealSamplingRate(from: NSRange(location: 0, length: NSInteger(maximumSamplingRate)))
|
let selectedSamplingRate = self.machine.idealSamplingRate(from: NSRange(location: 0, length: NSInteger(maximumSamplingRate)))
|
||||||
@ -135,97 +235,15 @@ class MachineDocument:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func close() {
|
/// Responds to the CSAudioQueueDelegate dry-queue warning message by requesting a machine update.
|
||||||
activityPanel?.setIsVisible(false)
|
|
||||||
activityPanel = nil
|
|
||||||
|
|
||||||
optionsPanel?.setIsVisible(false)
|
|
||||||
optionsPanel = nil
|
|
||||||
|
|
||||||
bestEffortLock.lock()
|
|
||||||
if let bestEffortUpdater = bestEffortUpdater {
|
|
||||||
bestEffortUpdater.delegate = nil
|
|
||||||
bestEffortUpdater.flush()
|
|
||||||
self.bestEffortUpdater = nil
|
|
||||||
}
|
|
||||||
bestEffortLock.unlock()
|
|
||||||
|
|
||||||
actionLock.lock()
|
|
||||||
drawLock.lock()
|
|
||||||
machine = nil
|
|
||||||
openGLView.delegate = nil
|
|
||||||
openGLView.invalidate()
|
|
||||||
actionLock.unlock()
|
|
||||||
drawLock.unlock()
|
|
||||||
|
|
||||||
super.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: configuring
|
|
||||||
fileprivate var missingROMs: [CSMissingROM] = []
|
|
||||||
fileprivate var selectedMachine: CSStaticAnalyser?
|
|
||||||
|
|
||||||
func configureAs(_ analysis: CSStaticAnalyser) {
|
|
||||||
let missingROMs = NSMutableArray()
|
|
||||||
if let machine = CSMachine(analyser: analysis, missingROMs: missingROMs) {
|
|
||||||
self.selectedMachine = nil
|
|
||||||
self.machine = machine
|
|
||||||
self.optionsPanelNibName = analysis.optionsPanelNibName
|
|
||||||
setupMachineOutput()
|
|
||||||
setupActivityDisplay()
|
|
||||||
} else {
|
|
||||||
// Store the selected machine and list of missing ROMs, and
|
|
||||||
// show the missing ROMs dialogue.
|
|
||||||
self.missingROMs = []
|
|
||||||
for untypedMissingROM in missingROMs {
|
|
||||||
self.missingROMs.append(untypedMissingROM as! CSMissingROM)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.selectedMachine = analysis
|
|
||||||
requestRoms()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate var shouldShowNewMachinePanel = false
|
|
||||||
override func read(from url: URL, ofType typeName: String) throws {
|
|
||||||
if let analyser = CSStaticAnalyser(fileAt: url) {
|
|
||||||
self.displayName = analyser.displayName
|
|
||||||
self.configureAs(analyser)
|
|
||||||
} else {
|
|
||||||
throw NSError(domain: "MachineDocument", code: -1, userInfo: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
convenience init(type typeName: String) throws {
|
|
||||||
self.init()
|
|
||||||
self.fileType = typeName
|
|
||||||
self.shouldShowNewMachinePanel = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: the pasteboard
|
|
||||||
func paste(_ sender: Any) {
|
|
||||||
let pasteboard = NSPasteboard.general
|
|
||||||
if let string = pasteboard.string(forType: .string) {
|
|
||||||
self.machine.paste(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: CSBestEffortUpdaterDelegate
|
|
||||||
final func bestEffortUpdater(_ bestEffortUpdater: CSBestEffortUpdater!, runForInterval duration: TimeInterval, didSkipPreviousUpdate: Bool) {
|
|
||||||
if actionLock.try() {
|
|
||||||
self.machine.run(forInterval: duration)
|
|
||||||
actionLock.unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: CSAudioQueueDelegate
|
|
||||||
final func audioQueueIsRunningDry(_ audioQueue: CSAudioQueue) {
|
final func audioQueueIsRunningDry(_ audioQueue: CSAudioQueue) {
|
||||||
bestEffortLock.lock()
|
bestEffortLock.lock()
|
||||||
bestEffortUpdater?.update()
|
bestEffortUpdater?.update()
|
||||||
bestEffortLock.unlock()
|
bestEffortLock.unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: CSOpenGLViewDelegate
|
/// Responds to the CSOpenGLViewDelegate redraw message by requesting a machine update if this is a timed
|
||||||
|
/// request, and ordering a redraw regardless of the motivation.
|
||||||
final func openGLViewRedraw(_ view: CSOpenGLView, event redrawEvent: CSOpenGLViewRedrawEvent) {
|
final func openGLViewRedraw(_ view: CSOpenGLView, event redrawEvent: CSOpenGLViewRedrawEvent) {
|
||||||
if redrawEvent == .timer {
|
if redrawEvent == .timer {
|
||||||
bestEffortLock.lock()
|
bestEffortLock.lock()
|
||||||
@ -246,7 +264,27 @@ class MachineDocument:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Runtime media insertion.
|
/// Responds to CSBestEffortUpdaterDelegate update message by running the machine.
|
||||||
|
final func bestEffortUpdater(_ bestEffortUpdater: CSBestEffortUpdater!, runForInterval duration: TimeInterval, didSkipPreviousUpdate: Bool) {
|
||||||
|
if let machine = self.machine, actionLock.try() {
|
||||||
|
machine.run(forInterval: duration)
|
||||||
|
actionLock.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Pasteboard Forwarding.
|
||||||
|
|
||||||
|
/// Forwards any text currently on the pasteboard into the active machine.
|
||||||
|
func paste(_ sender: Any) {
|
||||||
|
let pasteboard = NSPasteboard.general
|
||||||
|
if let string = pasteboard.string(forType: .string), let machine = self.machine {
|
||||||
|
machine.paste(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Runtime Media Insertion.
|
||||||
|
|
||||||
|
/// Delegate message to receive drag and drop files.
|
||||||
final func openGLView(_ view: CSOpenGLView, didReceiveFileAt URL: URL) {
|
final func openGLView(_ view: CSOpenGLView, didReceiveFileAt URL: URL) {
|
||||||
let mediaSet = CSMediaSet(fileAt: URL)
|
let mediaSet = CSMediaSet(fileAt: URL)
|
||||||
if let mediaSet = mediaSet {
|
if let mediaSet = mediaSet {
|
||||||
@ -254,6 +292,8 @@ class MachineDocument:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Action for the insert menu command; displays an NSOpenPanel and then segues into the same process
|
||||||
|
/// as if a file had been received via drag and drop.
|
||||||
@IBAction final func insertMedia(_ sender: AnyObject!) {
|
@IBAction final func insertMedia(_ sender: AnyObject!) {
|
||||||
let openPanel = NSOpenPanel()
|
let openPanel = NSOpenPanel()
|
||||||
openPanel.message = "Hint: you can also insert media by dragging and dropping it onto the machine's window."
|
openPanel.message = "Hint: you can also insert media by dragging and dropping it onto the machine's window."
|
||||||
@ -269,12 +309,10 @@ class MachineDocument:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: NSDocument overrides
|
// MARK: - Input Management.
|
||||||
override func data(ofType typeName: String) throws -> Data {
|
|
||||||
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Input management
|
/// Upon a resign key, immediately releases all ongoing input mechanisms — any currently pressed keys,
|
||||||
|
/// and joystick and mouse inputs.
|
||||||
func windowDidResignKey(_ notification: Notification) {
|
func windowDidResignKey(_ notification: Notification) {
|
||||||
if let machine = self.machine {
|
if let machine = self.machine {
|
||||||
machine.clearAllKeys()
|
machine.clearAllKeys()
|
||||||
@ -283,24 +321,28 @@ class MachineDocument:
|
|||||||
self.openGLView.releaseMouse()
|
self.openGLView.releaseMouse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Upon becoming key, attaches joystick input to the machine.
|
||||||
func windowDidBecomeKey(_ notification: Notification) {
|
func windowDidBecomeKey(_ notification: Notification) {
|
||||||
if let machine = self.machine {
|
if let machine = self.machine {
|
||||||
machine.joystickManager = (DocumentController.shared as! DocumentController).joystickManager
|
machine.joystickManager = (DocumentController.shared as! DocumentController).joystickManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forwards key down events directly to the machine.
|
||||||
func keyDown(_ event: NSEvent) {
|
func keyDown(_ event: NSEvent) {
|
||||||
if let machine = self.machine {
|
if let machine = self.machine {
|
||||||
machine.setKey(event.keyCode, characters: event.characters, isPressed: true)
|
machine.setKey(event.keyCode, characters: event.characters, isPressed: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forwards key up events directly to the machine.
|
||||||
func keyUp(_ event: NSEvent) {
|
func keyUp(_ event: NSEvent) {
|
||||||
if let machine = self.machine {
|
if let machine = self.machine {
|
||||||
machine.setKey(event.keyCode, characters: event.characters, isPressed: false)
|
machine.setKey(event.keyCode, characters: event.characters, isPressed: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Synthesies appropriate key up and key down events upon any change in modifiers.
|
||||||
func flagsChanged(_ newModifiers: NSEvent) {
|
func flagsChanged(_ newModifiers: NSEvent) {
|
||||||
if let machine = self.machine {
|
if let machine = self.machine {
|
||||||
machine.setKey(VK_Shift, characters: nil, isPressed: newModifiers.modifierFlags.contains(.shift))
|
machine.setKey(VK_Shift, characters: nil, isPressed: newModifiers.modifierFlags.contains(.shift))
|
||||||
@ -310,31 +352,34 @@ class MachineDocument:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forwards mouse movement events to the mouse.
|
||||||
func mouseMoved(_ event: NSEvent) {
|
func mouseMoved(_ event: NSEvent) {
|
||||||
if let machine = self.machine {
|
if let machine = self.machine {
|
||||||
machine.addMouseMotionX(event.deltaX, y: event.deltaY)
|
machine.addMouseMotionX(event.deltaX, y: event.deltaY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forwards mouse button down events to the mouse.
|
||||||
func mouseUp(_ event: NSEvent) {
|
func mouseUp(_ event: NSEvent) {
|
||||||
if let machine = self.machine {
|
if let machine = self.machine {
|
||||||
machine.setMouseButton(Int32(event.buttonNumber), isPressed: false)
|
machine.setMouseButton(Int32(event.buttonNumber), isPressed: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forwards mouse button up events to the mouse.
|
||||||
func mouseDown(_ event: NSEvent) {
|
func mouseDown(_ event: NSEvent) {
|
||||||
if let machine = self.machine {
|
if let machine = self.machine {
|
||||||
machine.setMouseButton(Int32(event.buttonNumber), isPressed: true)
|
machine.setMouseButton(Int32(event.buttonNumber), isPressed: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: New machine creation.
|
// MARK: - MachinePicker Outlets and Actions
|
||||||
@IBOutlet var machinePicker: MachinePicker?
|
@IBOutlet var machinePicker: MachinePicker?
|
||||||
@IBOutlet var machinePickerPanel: NSWindow?
|
@IBOutlet var machinePickerPanel: NSWindow?
|
||||||
@IBAction func createMachine(_ sender: NSButton?) {
|
@IBAction func createMachine(_ sender: NSButton?) {
|
||||||
let selectedMachine = machinePicker!.selectedMachine()
|
let selectedMachine = machinePicker!.selectedMachine()
|
||||||
self.windowControllers[0].window?.endSheet(self.machinePickerPanel!)
|
self.windowControllers[0].window?.endSheet(self.machinePickerPanel!)
|
||||||
machinePicker = nil
|
self.machinePicker = nil
|
||||||
self.configureAs(selectedMachine)
|
self.configureAs(selectedMachine)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,7 +387,7 @@ class MachineDocument:
|
|||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: User ROM provision.
|
// MARK: - ROMRequester Outlets and Actions
|
||||||
@IBOutlet var romRequesterPanel: NSWindow?
|
@IBOutlet var romRequesterPanel: NSWindow?
|
||||||
@IBOutlet var romRequesterText: NSTextField?
|
@IBOutlet var romRequesterText: NSTextField?
|
||||||
@IBOutlet var romReceiverErrorField: NSTextField?
|
@IBOutlet var romReceiverErrorField: NSTextField?
|
||||||
@ -446,7 +491,7 @@ class MachineDocument:
|
|||||||
if didInstallRom {
|
if didInstallRom {
|
||||||
if self.missingROMs.count == 0 {
|
if self.missingROMs.count == 0 {
|
||||||
self.windowControllers[0].window?.endSheet(self.romRequesterPanel!)
|
self.windowControllers[0].window?.endSheet(self.romRequesterPanel!)
|
||||||
configureAs(self.selectedMachine!)
|
configureAs(self.machineDescription!)
|
||||||
} else {
|
} else {
|
||||||
populateMissingRomList()
|
populateMissingRomList()
|
||||||
}
|
}
|
||||||
@ -509,6 +554,9 @@ class MachineDocument:
|
|||||||
machine.inputMode = .joystick
|
machine.inputMode = .joystick
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determines which of the menu items to enable and disable based on the ability of the
|
||||||
|
/// current machine to handle keyboard and joystick input, accept new media and whether
|
||||||
|
/// it has an associted activity window.
|
||||||
override func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
override func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
||||||
if let menuItem = item as? NSMenuItem {
|
if let menuItem = item as? NSMenuItem {
|
||||||
switch item.action {
|
switch item.action {
|
||||||
@ -542,7 +590,7 @@ class MachineDocument:
|
|||||||
return super.validateUserInterfaceItem(item)
|
return super.validateUserInterfaceItem(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Screenshot capture.
|
/// Saves a screenshot of the
|
||||||
@IBAction func saveScreenshot(_ sender: AnyObject!) {
|
@IBAction func saveScreenshot(_ sender: AnyObject!) {
|
||||||
// Grab a date formatter and form a file name.
|
// Grab a date formatter and form a file name.
|
||||||
let dateFormatter = DateFormatter()
|
let dateFormatter = DateFormatter()
|
||||||
@ -566,7 +614,8 @@ class MachineDocument:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Activity display.
|
// MARK: Activity display.
|
||||||
class LED {
|
|
||||||
|
private class LED {
|
||||||
let levelIndicator: NSLevelIndicator
|
let levelIndicator: NSLevelIndicator
|
||||||
init(levelIndicator: NSLevelIndicator) {
|
init(levelIndicator: NSLevelIndicator) {
|
||||||
self.levelIndicator = levelIndicator
|
self.levelIndicator = levelIndicator
|
||||||
@ -574,7 +623,8 @@ class MachineDocument:
|
|||||||
var isLit = false
|
var isLit = false
|
||||||
var isBlinking = false
|
var isBlinking = false
|
||||||
}
|
}
|
||||||
fileprivate var leds: [String: LED] = [:]
|
private var leds: [String: LED] = [:]
|
||||||
|
|
||||||
func setupActivityDisplay() {
|
func setupActivityDisplay() {
|
||||||
var leds = machine.leds
|
var leds = machine.leds
|
||||||
if leds.count > 0 {
|
if leds.count > 0 {
|
||||||
|
Loading…
Reference in New Issue
Block a user