// // ViewController.swift // Steve ][ // // Created by Tamas Rudnai on 7/25/19. // Copyright Β© 2019, 2020 Tamas Rudnai. All rights reserved. // // This file is part of Steve ][ -- The Apple ][ Emulator. // // Steve ][ is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Steve ][ is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Steve ][. If not, see . // import Cocoa import AVFoundation let K : Double = 1000.0 let M : Double = (K * K) let G : Double = (M * K) let T : Double = (G * K) let KB : Double = 1024.0 let MB : Double = (KB * KB) let GB : Double = (MB * KB) let TB : Double = (GB * KB) let colorWhite = NSColor.init( red:0.9296875, green:0.9296875, blue:0.9296875, alpha: 1 ) let colorGreen = NSColor.init( red:0.16796875, green:0.84375, blue:0.2890625, alpha: 1 ) let colorOrange = NSColor.init( red:1, green:0.38671875, blue:0.0078125, alpha: 1 ) var monoColor = colorGreen; var spk_up: AVAudioPlayer? var spk_dn: AVAudioPlayer? @_cdecl("ViewController_spk_up_play") func spk_up_play() { spk_up?.stop() spk_dn?.stop() spk_up?.play() } @_cdecl("ViewController_spk_dn_play") func spk_dn_play() { spk_up?.stop() spk_dn?.stop() spk_dn?.play() } //#if METAL_YES //import Metal //#endif class ViewController: NSViewController { @IBOutlet weak var displayField: NSTextField! @IBOutlet weak var display: NSTextFieldCell! @IBOutlet weak var speedometer: NSTextFieldCell! @IBOutlet weak var lores: LoRes! @IBOutlet weak var hires: HiRes! @IBOutlet weak var splashScreen: NSView! @IBOutlet weak var scanLines: NSImageView! var CRTMonitor = false var ColorMonitor = true var Keyboard2Joystick = true var Mouse2Joystick = false var MouseInterface = true // static let charConvStr : String = // "@πŸ„°πŸ„±πŸ„²πŸ„³πŸ„΄πŸ„΅πŸ„ΆπŸ„·πŸ„ΈπŸ„ΉπŸ„ΊπŸ„»πŸ„ΌπŸ„½πŸ„ΎπŸ„ΏπŸ…€πŸ…πŸ…‚πŸ…ƒπŸ…„πŸ……πŸ…†πŸ…‡πŸ…ˆπŸ…‰[\\]^_ !\"#$%&'()*+,-./0123456789:;<=>?" + // "@πŸ…°πŸ…±πŸ…²πŸ…³πŸ…΄πŸ…΅πŸ…ΆπŸ…·πŸ…ΈπŸ…ΉπŸ…ΊπŸ…»πŸ…ΌπŸ…½πŸ…ΎπŸ…ΏπŸ†€πŸ†πŸ†‚πŸ†ƒπŸ†„πŸ†…πŸ††πŸ†‡πŸ†ˆπŸ†‰[\\]^_β¬›οΈŽ!\"#$%&'()*+,-./0123456789:;<=>?" + // FL // "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !\"#$%&'()*+,-./0123456789:;<=>?" + // "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~?" // static let charConvStr : String = // "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !\"#$%&'()*+,-./0123456789:;<=>?" + // "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_β–‘!\"#$%&'()*+,-./0123456789:;<=>?" + // FL // "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_\u{E0A0}!\"#$%&'()*+,-./0123456789:;<=>?" + // "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~?" // static let charConvStr : String = // "\u{E140}\u{E141}\u{E142}\u{E143}\u{E144}\u{E145}\u{E146}\u{E147}\u{E148}\u{E149}\u{E14A}\u{E14B}\u{E14C}\u{E14D}\u{E14E}\u{E14F}\u{E150}\u{E151}\u{E152}\u{E153}\u{E154}\u{E155}\u{E156}\u{E157}\u{E158}\u{E159}\u{E15A}\u{E15B}\u{E15C}\u{E15D}\u{E15E}\u{E15F}\u{E120}\u{E121}\u{E122}\u{E123}\u{E124}\u{E125}\u{E126}\u{E127}\u{E128}\u{E129}\u{E12A}\u{E12B}\u{E12C}\u{E12D}\u{E12E}\u{E12F}\u{E130}\u{E131}\u{E132}\u{E133}\u{E134}\u{E135}\u{E136}\u{E137}\u{E138}\u{E139}\u{E13A}\u{E13B}\u{E13C}\u{E13D}\u{E13E}\u{E13F}" + // "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_β–‘!\"#$%&'()*+,-./0123456789:;<=>?" + // FL // "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_\u{E0A0}!\"#$%&'()*+,-./0123456789:;<=>?" + // "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~?" static let charConvStrFlashOff : String = "\u{E140}\u{E141}\u{E142}\u{E143}\u{E144}\u{E145}\u{E146}\u{E147}\u{E148}\u{E149}\u{E14A}\u{E14B}\u{E14C}\u{E14D}\u{E14E}\u{E14F}\u{E150}\u{E151}\u{E152}\u{E153}\u{E154}\u{E155}\u{E156}\u{E157}\u{E158}\u{E159}\u{E15A}\u{E15B}\u{E15C}\u{E15D}\u{E15E}\u{E15F}\u{E120}\u{E121}\u{E122}\u{E123}\u{E124}\u{E125}\u{E126}\u{E127}\u{E128}\u{E129}\u{E12A}\u{E12B}\u{E12C}\u{E12D}\u{E12E}\u{E12F}\u{E130}\u{E131}\u{E132}\u{E133}\u{E134}\u{E135}\u{E136}\u{E137}\u{E138}\u{E139}\u{E13A}\u{E13B}\u{E13C}\u{E13D}\u{E13E}\u{E13F}" + "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !\"#$%&'()*+,-./0123456789:;<=>?" + // FL "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !\"#$%&'()*+,-./0123456789:;<=>?" + "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u{E27F}" static let charConvStrFlashOn : String = "\u{E140}\u{E141}\u{E142}\u{E143}\u{E144}\u{E145}\u{E146}\u{E147}\u{E148}\u{E149}\u{E14A}\u{E14B}\u{E14C}\u{E14D}\u{E14E}\u{E14F}\u{E150}\u{E151}\u{E152}\u{E153}\u{E154}\u{E155}\u{E156}\u{E157}\u{E158}\u{E159}\u{E15A}\u{E15B}\u{E15C}\u{E15D}\u{E15E}\u{E15F}\u{E120}\u{E121}\u{E122}\u{E123}\u{E124}\u{E125}\u{E126}\u{E127}\u{E128}\u{E129}\u{E12A}\u{E12B}\u{E12C}\u{E12D}\u{E12E}\u{E12F}\u{E130}\u{E131}\u{E132}\u{E133}\u{E134}\u{E135}\u{E136}\u{E137}\u{E138}\u{E139}\u{E13A}\u{E13B}\u{E13C}\u{E13D}\u{E13E}\u{E13F}" + "\u{E140}\u{E141}\u{E142}\u{E143}\u{E144}\u{E145}\u{E146}\u{E147}\u{E148}\u{E149}\u{E14A}\u{E14B}\u{E14C}\u{E14D}\u{E14E}\u{E14F}\u{E150}\u{E151}\u{E152}\u{E153}\u{E154}\u{E155}\u{E156}\u{E157}\u{E158}\u{E159}\u{E15A}\u{E15B}\u{E15C}\u{E15D}\u{E15E}\u{E15F}\u{E120}\u{E121}\u{E122}\u{E123}\u{E124}\u{E125}\u{E126}\u{E127}\u{E128}\u{E129}\u{E12A}\u{E12B}\u{E12C}\u{E12D}\u{E12E}\u{E12F}\u{E130}\u{E131}\u{E132}\u{E133}\u{E134}\u{E135}\u{E136}\u{E137}\u{E138}\u{E139}\u{E13A}\u{E13B}\u{E13C}\u{E13D}\u{E13E}\u{E13F}" + "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !\"#$%&'()*+,-./0123456789:;<=>?" + "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u{E27F}" static let charConvTblFlashOn = Array( charConvStrFlashOn ) static let charConvTblFlashOff = Array( charConvStrFlashOff ) static var charConvTbl = charConvTblFlashOn static var romFileName = "Apple2e_Enhanced.rom"; static let textLineOfs : [Int] = [ 0x000, 0x080, 0x100, 0x180, 0x200, 0x280, 0x300, 0x380, 0x028, 0x0A8, 0x128, 0x1A8, 0x228, 0x2A8, 0x328, 0x3A8, 0x050, 0x0D0, 0x150, 0x1D0, 0x250, 0x2D0, 0x350, 0x3D0 ] func dialogOK(title: String, text: String) { let alert = NSAlert() alert.messageText = title alert.informativeText = text alert.alertStyle = .warning alert.addButton(withTitle: "OK") alert.runModal() // return alert.runModal() == .alertFirstButtonReturn } @objc func chk_woz_load(err : Int32) { switch (err) { case WOZ_ERR_OK: break case WOZ_ERR_FILE_NOT_FOUND: dialogOK(title: "Error Loading Disk Image", text: "File Not Found!") break case WOZ_ERR_NOT_WOZ_FILE: dialogOK(title: "Error Loading Disk Image", text: "File is not a WOZ image!") break case WOZ_ERR_BAD_CHUNK_HDR, WOZ_ERR_BAD_DATA: dialogOK(title: "Error Loading Disk Image", text: "Malformed WOZ image!") break default: dialogOK(title: "Error Loading Disk Image", text: "Unknown Error! (\(err))" ) break } } var workItem : DispatchWorkItem? = nil; @IBAction func PowerOn(_ sender: Any) { upd.suspend() cpuState = cpuState_inited; spkr_stopAll() //------------------------------------------------------------ // Animated Splash Screen fade out and (Text) Monitor fade in hires.isHidden = true displayField.alphaValue = 0 displayField.isHidden = false splashScreen.alphaValue = 1 splashScreen.isHidden = false DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { NSAnimationContext.runAnimationGroup({ (context) in context.duration = 0.5 // Use the value you want to animate to (NOT the starting value) self.displayField.animator().alphaValue = 1 self.splashScreen.animator().alphaValue = 0 }, completionHandler:{ () -> Void in self.displayField.alphaValue = 1 self.splashScreen.isHidden = true }) m6502_ColdReset( Bundle.main.resourcePath! + "/rom/", ViewController.romFileName ) cpuState = cpuState_running; self.upd.resume() } //------------------------------------------------------------ #if SPEEDTEST if ( workItem != nil ) { workItem!.cancel(); workItem = nil; } else { workItem = DispatchWorkItem { DispatchQueue.global(qos: .userInteractive).async { // DispatchQueue.global(qos: .userInitiated).async { // DispatchQueue.global(qos: .background).async { tst6502() } } DispatchQueue.global().async(execute: workItem!); } #else #endif } @IBAction func PowerOff(_ sender: Any) { upd.suspend() cpuState = cpuState_inited; spkr_stopAll() //------------------------------------------------------------ // Animated Splash Screen fade out and (Text) Monitor fade in hires.isHidden = true displayField.alphaValue = 0 displayField.isHidden = false splashScreen.alphaValue = 1 splashScreen.isHidden = false //------------------------------------------------------------ } @IBAction func Pause(_ sender: Any) { switch ( cpuState ) { case cpuState_halted: upd.resume() cpuState = cpuState_running break case cpuState_running: upd.suspend() cpuState = cpuState_halted break default: // upd.suspend() // cpuState = cpuState_halted break } } @IBAction func Reset(_ sender: Any) { // m6502.interrupt = SOFTRESET; // let saved_frm_set = clk_6502_per_frm_set; // clk_6502_per_frm_set = 0 // clk_6502_per_frm_max = 0 // // wait for 1 ms to allow the simulation to halt // usleep(10000); softReset() // clk_6502_per_frm_set = saved_frm_set } static let textPage1Addr = 0x400 static let textPage2Addr = 0x800 static let textBufferSize = 0x400 // static only needed to be able to initialize things static let textLines = 24 static let textCols = 40 static let lineEndChars = 1 // these are needed to be able to easy access to these constants from instances let textLines = ViewController.textLines let textCols = ViewController.textCols let lineEndChars = ViewController.lineEndChars var frameCnt = 0 // let spaceChar : Character = "\u{E17F}" // let blockChar : Character = "\u{E07F}" // static let spaceChar : Character = " " // static let blockChar : Character = "β–‘" // static var flashingSpace : Character = " " let ramBufferPointer = UnsafeRawBufferPointer(start: MEM, count: 64 * 1024) static let textPage1Pointer = UnsafeRawBufferPointer(start: MEM + textPage1Addr, count: textBufferSize) static let textPage2Pointer = UnsafeRawBufferPointer(start: MEM + textPage2Addr, count: textBufferSize) static let textIntBufferPointer = UnsafeRawBufferPointer(start: RAM + textPage1Addr, count: textBufferSize) static let textAuxBufferPointer = UnsafeRawBufferPointer(start: AUX + textPage1Addr, count: textBufferSize) // TODO: Render text screen in native C // static let textScreen = UnsafeMutableRawPointer(mutating: testText) var textBufferPointer = textPage1Pointer static let textArraySize = textLines * (textCols + lineEndChars) + textCols * 2 var txtClear = [Character](repeating: " ", count: textArraySize * 2) var txtArr = [Character](repeating: " ", count: textArraySize * 2) var s = String() func HexDump() { var txt : String = "" for y in 0...textLines - 1 { txt += String(format: "%04X: ", y * 16) for x in 0...15 { let byte = ramBufferPointer[ y * 16 + x ] let chr = String(format: "%02X ", byte) txt += chr } txt += "\n" } DispatchQueue.main.async { self.display.stringValue = txt self.speedometer.stringValue = String(format: "%0.3lf MHz", mhz); } } // AppleScript Keycodes let leftArrowKey = 123 let rightArrowKey = 124 let upArrowKey = 126 let downArrowKey = 125 var ddd = 9; override var acceptsFirstResponder: Bool { get { return true } } func SelectAll() { // displayField.currentEditor()?.selectAll(nil) displayField.selectText(nil) } func Copy() { let pasteBoard = NSPasteboard.general pasteBoard.clearContents() // TODO: Find a better way to avoid index out of range error when the entire text area is selected let string = display.stringValue + " " if let selectedRange = displayField.currentEditor()?.selectedRange { let startIndex = string.index(string.startIndex, offsetBy: selectedRange.lowerBound) let endIndex = string.index(string.startIndex, offsetBy: selectedRange.upperBound) let selectedString = string[startIndex..= fps ) { ViewController.charConvTbl = ViewController.charConvTblFlashOff frameCnt = 0 } // Rendering is happening in the main thread, which has two implications: // 1. We can update UI elements // 2. it is independent of the simulation, de that is running in the background thread while we are busy with rendering... DispatchQueue.main.sync { var txt : String = "" var fromLines = 0 var toLines = self.textLines if videoMode.text == 0 { if videoMode.mixed == 1 { fromLines = toLines - 4 } else { toLines = 0 } } self.txtArr = self.txtClear // render an empty space to eiminate displaying text portion of the screen covered by graphics let charDisposition = videoMode.col80 == 0 ? 1 : 2 for y in 0 ..< fromLines { self.txtArr[ y * (self.textCols * charDisposition + self.lineEndChars) + self.textCols * charDisposition] = "\n" } // 40 col if videoMode.col80 == 0 { if MEMcfg.txt_page_2 == 0 { self.textBufferPointer = ViewController.textPage1Pointer } else { self.textBufferPointer = ViewController.textPage2Pointer } // render the rest of the text screen for y in fromLines ..< toLines { for x in 0 ..< self.textCols { let byte = self.textBufferPointer[ ViewController.textLineOfs[y] + x ] let idx = Int(byte); let chr = ViewController.charConvTbl[idx] self.txtArr[ y * (self.textCols + self.lineEndChars) + x ] = chr } self.txtArr[ y * (self.textCols + self.lineEndChars) + self.textCols ] = "\n" } } // 80 col else { let auxPage = ( MEMcfg.is_80STORE == 1 ) && ( MEMcfg.txt_page_2 == 1 ) let textIntBuffer = auxPage ? ViewController.textIntBufferPointer : ViewController.textPage1Pointer let textAuxBuffer = auxPage ? ViewController.textPage1Pointer : ViewController.textAuxBufferPointer // render the rest of the text screen for y in fromLines ..< toLines { for x in 0 ..< self.textCols { let byte = textIntBuffer[ ViewController.textLineOfs[y] + x ] let idx = Int(byte); let chr = ViewController.charConvTbl[idx] self.txtArr[ y * (self.textCols * 2 + self.lineEndChars) + x * 2 + 1] = chr let byte2 = textAuxBuffer[ ViewController.textLineOfs[y] + x ] let idx2 = Int(byte2); let chr2 = ViewController.charConvTbl[idx2] self.txtArr[ y * (self.textCols * 2 + self.lineEndChars) + x * 2] = chr2 } self.txtArr[ y * (self.textCols * 2 + self.lineEndChars) + self.textCols * 2] = "\n" } } txt = String(self.txtArr) // TODO: Render text Screen in native C // txt = String(bytesNoCopy: ViewController.textScreen!, length: 10, encoding: .ascii, freeWhenDone: false) ?? "HMM" if videoMode.col80 != self.currentVideoMode.col80 { self.currentVideoMode.col80 = videoMode.col80 if let fontSize = self.display.font?.pointSize { if videoMode.col80 == 1 { self.display.font = NSFont(name: "PRNumber3", size: fontSize) } else { self.display.font = NSFont(name: "PrintChar21", size: fontSize) } } } if ( self.shadowTxt != txt ) { self.shadowTxt = txt self.display.stringValue = txt } // self.display.stringValue = "testing\nit\nout" if ( (mhz < 1.5) && (mhz != floor(mhz)) ) { self.speedometer.stringValue = String(format: "%0.3lf MHz", mhz); } else { self.speedometer.stringValue = String(format: "%0.1lf MHz", mhz); } // else { // self.speedometer.stringValue = String(format: "%.0lf MHz", mhz); // } #if HIRES // only refresh graphics view when needed (aka not in text mode) if ( videoMode.text == 0 ) { if ( videoMode.hires == 0 ) { // when we change video mode, buffer needs to be cleared to avoid artifacts if ( self.savedVideoMode.text == 1 ) || ( self.savedVideoMode.mixed != videoMode.mixed ) || ( self.savedVideoMode.hires != videoMode.hires ) { self.lores.clearScreen() self.lores.isHidden = false self.hires.isHidden = true } self.lores.Render() } else { // when we change video mode, buffer needs to be cleared to avoid artifacts if ( self.savedVideoMode.text == 1 ) || ( self.savedVideoMode.mixed != videoMode.mixed ) || ( self.savedVideoMode.hires != videoMode.hires ) { self.hires.clearScreen() self.hires.isHidden = false self.lores.isHidden = true } hires.Render() } } else if ( self.savedVideoMode.text == 0 ) { // we just switched from grahics to text self.lores.isHidden = true self.hires.isHidden = true } self.savedVideoMode = videoMode #endif // stream speaker from a separate thread from the simulation // TODO: Do we need to do this from here? // spkr_update() } } override func mouseDown(with event: NSEvent) { if ( Mouse2Joystick ) { setIO(0xC061, 1 << 7) } } override func mouseUp(with event: NSEvent) { if ( Mouse2Joystick ) { setIO(0xC061, 0) } } override func rightMouseDown(with event: NSEvent) { if ( Mouse2Joystick ) { setIO(0xC062, 1 << 7) } } override func rightMouseUp(with event: NSEvent) { if ( Mouse2Joystick ) { setIO(0xC062, 0) } } override func otherMouseDown(with event: NSEvent) { if ( Mouse2Joystick ) { setIO(0xC063, 0) // inverted (bit 7: 0 = pressed) } } override func otherMouseUp(with event: NSEvent) { if ( Mouse2Joystick ) { setIO(0xC063, 1 << 7) // inverted (bit 7: 1 = not pressed) } } func Update() { switch cpuState { case cpuState_running: clkCounter += Double(clkfrm) // we start a new frame from here, so CPU is running even while rendering clkfrm = 0 frameCounter += 1 if ( frameCounter % fps == 0 ) { let currentTime = CACurrentMediaTime() as Double let elpasedTime = currentTime - lastFrameTime lastFrameTime = currentTime mhz = Double( clkCounter ) / (elpasedTime * M); clkCounter = 0 } #if SPEEDTEST #else // poll input devices like mouse and joystick // Input() // run some code m6502_Run() // video rendering if ( frameCounter % video_fps_divider == 0 ) { Render() } #endif break case cpuState_halting: cpuState = cpuState_halted // video rendering Render() break case cpuState_halted: upd.suspend() break default: break } } // func FromBuf(ptr: UnsafeMutablePointer, length len: Int) -> String? { // // convert the bytes using the UTF8 encoding // if let theString = NSString(bytes: ptr, length: len, encoding: NSUTF8StringEncoding) { // return theString as String // } else { // return nil // the bytes aren't valid UTF8 // } // } var upd = RepeatingTimer(timeInterval: 1) static var current : ViewController? = nil func newUpdateTimer( timeInterval : Double ) { upd.kill() upd = RepeatingTimer(timeInterval: timeInterval) upd.eventHandler = { self.Update() } upd.resume() } override func viewDidLoad() { super.viewDidLoad() ViewController.current = self hires.clearScreen(); spkr_load_sfx( Bundle.main.resourcePath! + "/sfx" ) let woz_err = woz_loadFile( Bundle.main.resourcePath! + "/dsk/Apple DOS 3.3 January 1983.woz" ) chk_woz_load(err: woz_err) woz_flags.image_file_readonly = 1 //view.frame = CGRect(origin: CGPoint(), size: NSScreen.main!.visibleFrame.size) // createHiRes() self.displayField.scaleUnitSquare(to: NSSize(width: 1, height: 1)) // NSEvent.removeMonitor(NSEvent.EventType.flagsChanged) // NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { // self.flagsChanged(with: $0) // return $0 // } // NSEvent.removeMonitor(NSEvent.EventType.keyDown) NSEvent.addLocalMonitorForEvents(matching: .keyDown) { // print("keyDown event") self.keyDown(with: $0) return nil // return $0 } NSEvent.addLocalMonitorForEvents(matching: .keyUp) { // print("keyUp event") self.keyUp(with: $0) return nil // return $0 } displayField.maximumNumberOfLines = textLines displayField.preferredMaxLayoutWidth = displayField.frame.width // DispatchQueue.main.asyncAfter(deadline: .now() + 1/fps, execute: { // self.update() // }) // #if FUNCTIONTEST // #else // DispatchQueue.global(qos: .background).async { // self.update() // } // upd.eventHandler = { // self.Update() // } // upd.resume() newUpdateTimer( timeInterval: 1 / Double(fps) ) // #endif } override func viewDidAppear() { displayField.currentEditor()?.selectedRange = NSMakeRange(0, 0) self.displayField.window?.makeFirstResponder(self) } func setCPUClockSpeed( freq : Double ) { MHz_6502 = freq clk_6502_per_frm = UInt64( MHz_6502 * M / Double(fps) ) clk_6502_per_frm_set = clk_6502_per_frm } @IBAction func speedSelected(_ sender: NSButton) { if ( sender.title == "MAX" ) { setCPUClockSpeed(freq: 1600) } else if let freq = Double( sender.title ) { setCPUClockSpeed(freq: freq) // TODO: Probably this is not the best way to deal with the problem: To make sound continous independent of FPS and Freq // spkr_extra_buf = Int32( 780 / fps ) spkr_extra_buf = 0 switch freq { case 2.0: // spkr_extra_buf = Int32( Double(spkr_extra_buf) * 2.961538461538462 ) // normally it should come up as 77, but this way it is calculated with FPS spkr_extra_buf = 120 break case 4.0: // spkr_extra_buf = Int32( Double(spkr_extra_buf) * 1.346153846153846 ) // normally it should come up as 35, but this way it is calculated with FPS spkr_extra_buf = 8 break default: // spkr_extra_buf = Int32( 780 / fps ) // normally it should come up as 26, but this way it is calculated with FPS spkr_extra_buf = 0 break } SoundGap.integerValue = Int(spkr_extra_buf) } } @IBOutlet weak var lab: NSTextFieldCell! @IBAction func extraBuf(_ sender: NSSlider) { spkr_extra_buf = sender.intValue lab.title = String( spkr_extra_buf ) } func setSimulationMode( mode : String ) { switch ( mode ) { case "Eco": cpuMode = cpuMode_eco fps = DEFAULT_FPS video_fps_divider = DEF_VIDEO_DIV break case "Game": cpuMode = cpuMode_game cpuState = cpuState_running fps = GAME_FPS video_fps_divider = GAME_VIDEO_DIV break default: cpuMode = cpuMode_normal cpuState = cpuState_running fps = DEFAULT_FPS video_fps_divider = DEF_VIDEO_DIV break } spkr_fps_divider = fps / spkr_fps spkr_play_timeout = 8 * spkr_fps_divider // spkr_buf_size = spkr_sample_rate * 2 / spkr_fps newUpdateTimer( timeInterval: 1 / Double(fps) ) setCPUClockSpeed(freq: MHz_6502) // TODO: Better way to deal with speaker!!! spkr_play_timeout = 8 * video_fps_divider } @IBAction func setCPUMode(_ sender: NSPopUpButton) { setSimulationMode(mode: sender.selectedItem?.title ?? "Normal" ) } @IBOutlet weak var SoundGap: NSTextFieldCell! @IBAction func SoundGapChanged(_ sender: NSStepper) { SoundGap.integerValue = sender.integerValue spkr_extra_buf = Int32( sender.integerValue ) } @IBAction func CRTMonitorOnOff(_ sender: NSButton) { CRTMonitor = sender.state == .on scanLines.isHidden = !CRTMonitor hires.RenderFullScreen() } func ColorMonitorSelector( color : Bool ) { ColorMonitor = color if ( ColorMonitor ) { display.textColor = colorWhite // .white } else { display.textColor = colorGreen // .green } hires.RenderFullScreen() } @IBAction func ColorMonitorOnOff(_ sender: NSButton) { ColorMonitorSelector( color: sender.state == .on ) } func MonoMonitorChange( color: String ) { switch color { case "Green": ColorMonitor = false monoColor = colorGreen hires.monoColor = hires.color_green case "Amber": ColorMonitor = false monoColor = colorOrange hires.monoColor = hires.color_orange default: ColorMonitor = false monoColor = colorWhite hires.monoColor = hires.color_white } display.textColor = monoColor hires.RenderFullScreen() } @IBAction func MonitorChange(_ sender: NSButton) { switch sender.title { case "Green Mono": MonoMonitorChange(color: "Green") case "Amber Mono": MonoMonitorChange(color: "Amber") default: MonoMonitorChange(color: "White") } } @IBAction func Keyboard2JoystickOnOff(_ sender: NSButton) { Keyboard2Joystick = sender.state == .on } @IBAction func Mouse2JoystickOnOff(_ sender: NSButton) { Mouse2Joystick = sender.state == .on } @IBAction func MouseOnOff(_ sender: NSButton) { MouseInterface = sender.state == .on } @IBAction func QuickDisk(_ sender: NSButton) { if sender.state == .on { diskAccelerator_enabled = 1; } else { diskAccelerator_enabled = 0; } } @IBAction func DiskSound(_ sender: NSButton) { if sender.state == .on { disk_sfx_enabled = 1; } else { disk_sfx_enabled = 0; } } func openDiskImage( url: URL ) { switch url.pathExtension.uppercased() { case "WOZ": let err = woz_loadFile( url.path ) if err == WOZ_ERR_OK { NSDocumentController.shared.noteNewRecentDocumentURL(URL(fileURLWithPath: url.path)) } else { self.chk_woz_load(err: err) } case "DSK", "DO", "PO" : woz_eject() let err = dsk2woz( url.path ) if err == WOZ_ERR_OK { let err = woz_parseBuffer() if err == WOZ_ERR_OK { NSDocumentController.shared.noteNewRecentDocumentURL(URL(fileURLWithPath: url.path)) } } else { self.chk_woz_load(err: err) } default: break } } @objc func openDiskImageDialog() { let openPanel = NSOpenPanel() openPanel.title = "Open Disk Image" openPanel.allowsMultipleSelection = false openPanel.canChooseDirectories = false openPanel.canCreateDirectories = false openPanel.canChooseFiles = true openPanel.allowedFileTypes = ["dsk","do","po","nib", "woz"] openPanel.begin { (result) -> Void in if result == NSApplication.ModalResponse.OK { // print("file:", openPanel.url!.path) //Do what you will //If there's only one URL, surely 'openPanel.URL' //but otherwise a for loop works if let url = openPanel.url { self.openDiskImage(url: url) } else { let a = NSAlert() a.messageText = "File Not Found" a.informativeText = "Could not locate selected file" a.alertStyle = .critical a.beginSheetModal( for: self.view.window! ) } } } } @objc func saveDiskImage() { let openPanel = NSOpenPanel() openPanel.title = "Save Disk Image" openPanel.allowsMultipleSelection = false openPanel.canChooseDirectories = false openPanel.canCreateDirectories = false openPanel.canChooseFiles = true openPanel.allowedFileTypes = ["dsk","do","po","nib", "woz"] openPanel.begin { (result) -> Void in if result == NSApplication.ModalResponse.OK { // print("file:", openPanel.url!.path) //Do what you will //If there's only one URL, surely 'openPanel.URL' //but otherwise a for loop works if let filePath = openPanel.url?.path { let woz_err = woz_saveFile( filePath ) if woz_err == WOZ_ERR_OK { NSDocumentController.shared.noteNewRecentDocumentURL(URL(fileURLWithPath: filePath)) } else { self.chk_woz_load(err: woz_err) } } } } } @IBOutlet weak var QuickDisk_Disk1: NSMenuItem! @IBOutlet weak var QuickDisk_Disk2: NSMenuItem! @IBOutlet weak var DiskSound_Disk1: NSMenuItem! @IBOutlet weak var DiskSound_Disk2: NSMenuItem! @IBAction func Disk1(_ sender: NSPopUpButton) { switch sender.selectedItem?.tag { case 1: // Open openDiskImageDialog() case 2: // Save saveFile() case 3: // Save As... saveFileAs() case 4: // Eject woz_eject() case 21: // Quick Disk if diskAccelerator_enabled == 0 { diskAccelerator_enabled = 1; } else { diskAccelerator_enabled = 0; } let state : NSControl.StateValue = diskAccelerator_enabled == 1 ? .on : .off QuickDisk_Disk1?.state = state QuickDisk_Disk2?.state = state break case 22: // Disk Sound if disk_sfx_enabled == 0 { disk_sfx_enabled = 1; } else { disk_sfx_enabled = 0; } let state : NSControl.StateValue = disk_sfx_enabled == 1 ? .on : .off DiskSound_Disk1?.state = state DiskSound_Disk2?.state = state break case 1000: // Open Default Disk Image if let menuIdentifier = sender.selectedItem?.title { let woz_err = woz_loadFile( Bundle.main.resourcePath! + "/dsk/" + menuIdentifier + ".woz" ) ViewController.current?.chk_woz_load(err: woz_err) woz_flags.image_file_readonly = 1 } default: break } } @IBAction func traceEnable(_ sender: NSButton) { switch sender.state { case .on: m6502.dbgLevel.trace = 1 openLog() default: m6502.dbgLevel.trace = 0 closeLog() } } func saveFile() { if ( woz_flags.image_file_readonly != 0 ) { // it is readonly, save it to a different file... saveFileAs() } else { // save WOZ image file overwriting the original image woz_saveFile(nil) } } func saveFileAs() { let savePanel = NSSavePanel() savePanel.title = "Save WOZ Disk Image As..." savePanel.begin { (result) in if result.rawValue == NSApplication.ModalResponse.OK.rawValue { woz_saveFile( savePanel.url?.path ); } else { let a = NSAlert() a.messageText = "Are you sure?" a.informativeText = "Are you sure you would like to cancel and lose all modification you have made to the Disk Image?\nALERT: You will lose all new files and modified files from this Disk Image since you loaded. Your decision if permanent and irreversible!" a.addButton(withTitle: "Save") a.addButton(withTitle: "Cancel") a.alertStyle = .warning a.beginSheetModal(for: self.view.window!, completionHandler: { (modalResponse) -> Void in if modalResponse == .alertFirstButtonReturn { self.saveFileAs() } }) } } } } @_cdecl("woz_ask_to_save") func woz_ask_to_save() { ViewController.current?.saveFile() }