From f73303c95aba47fbf9f82da881f671445a119a67 Mon Sep 17 00:00:00 2001 From: Jeff Parsons Date: Sat, 16 Jan 2016 14:38:37 -0800 Subject: [PATCH] Added support for an optional "power" button --- modules/pcjs/lib/computer.js | 152 +++++++++++++++++++++++++---------- modules/pcjs/lib/cpu.js | 34 ++++---- 2 files changed, 128 insertions(+), 58 deletions(-) diff --git a/modules/pcjs/lib/computer.js b/modules/pcjs/lib/computer.js index 2ebd0f10d..c649bcec7 100644 --- a/modules/pcjs/lib/computer.js +++ b/modules/pcjs/lib/computer.js @@ -22,7 +22,7 @@ * * You are required to include the above copyright notice in every source code file of every * copy or modified version of this work, and to display that copyright notice on every screen - * that loads or runs any version of this software (see Computer.sCopyright). + * that loads or runs any version of this software (see Computer.COPYRIGHT). * * Some PCjs files also attempt to load external resource files, such as character-image files, * ROM files, and disk image files. Those external resource files are not considered part of the @@ -85,6 +85,9 @@ if (NODE) { * The Computer component has no required (parmsComputer) properties, but does * support the following: * + * autoPower: true to automatically power the computer (default), false to wait; + * false is honored only if a "power" button binding exists. + * * busWidth: number of memory address lines (address bits) on the computer's "bus"; * 20 is the minimum (and the default), which implies 8086/8088 real-mode addressing, * while 24 is required for 80286 protected-mode addressing. This value is passed @@ -121,10 +124,19 @@ function Computer(parmsComputer, parmsMachine, fSuspended) { Component.call(this, "Computer", parmsComputer, Computer, Messages.COMPUTER); this.aFlags.fPowered = false; + this.fAutoPower = parmsComputer['autoPower']; + + /* + * nPowerChange is 0 while the power state is stable, 1 while power is transitioning + * to "on", and -1 while power is transitioning to "off". + */ + this.nPowerChange = 0; + /* * TODO: Deprecate 'buswidth' (it should have always used camelCase) */ this.nBusWidth = parmsComputer['busWidth'] || parmsComputer['buswidth']; + this.resume = Computer.RESUME_NONE; this.sStateData = null; this.fStateData = false; // remembers if sStateData was loaded @@ -227,7 +239,7 @@ function Computer(parmsComputer, parmsMachine, fSuspended) { this.resume = Computer.RESUME_NONE; } if (this.resume) { - this.stateComputer = new State(this, Computer.sAppVer); + this.stateComputer = new State(this, Computer.APPVERSION); if (this.stateComputer.load()) { sStatePath = null; } else { @@ -251,12 +263,12 @@ function Computer(parmsComputer, parmsMachine, fSuspended) { web.loadResource(sStatePath, true, null, this, this.doneLoad); } - if (!fSuspended) { - /* - * Power "up" the computer, giving every component the opportunity to reset or restore itself. - */ - this.wait(this.powerOn); - } + if (!this.bindings["power"]) this.fAutoPower = true; + + /* + * Power on the computer, giving every component the opportunity to reset or restore itself. + */ + if (!fSuspended && this.fAutoPower) this.wait(this.powerOn); } Component.subclass(Computer); @@ -267,9 +279,9 @@ Component.subclass(Computer); * previous localStorage data be released with a version number that is at least 1 greater, * since we're tagging the localStorage data with the integer portion of the version string. */ -Computer.sAppName = APPNAME || "PCjs"; -Computer.sAppVer = APPVERSION; -Computer.sCopyright = "Copyright © 2012-2016 Jeff Parsons "; +Computer.APPNAME = APPNAME || "PCjs"; +Computer.APPVERSION = APPVERSION; +Computer.COPYRIGHT = "Copyright © 2012-2016 Jeff Parsons "; /* * I think it's a good idea to also display a GPL notice, putting people on notice that even @@ -290,11 +302,11 @@ Computer.STATE_USERID = "user"; * internal use only, and RESUME_DELETE is not documented (it provides a way of deleting ALL saved states * whenever a resume is declined). As a result, the only "end-user" values are 0, 1 and 2. */ -Computer.RESUME_REPOWER = -1; // resume without changing any state (for internal use only) -Computer.RESUME_NONE = 0; // default (no resume) -Computer.RESUME_AUTO = 1; // automatically save/restore state -Computer.RESUME_PROMPT = 2; // automatically save but conditionally restore (WARNING: if restore is declined, any state is discarded) -Computer.RESUME_DELETE = 3; // same as RESUME_PROMPT but discards ALL machines states whenever ANY machine restore is declined (undocumented) +Computer.RESUME_REPOWER = -1; // resume without changing any state (for internal use only) +Computer.RESUME_NONE = 0; // default (no resume) +Computer.RESUME_AUTO = 1; // automatically save/restore state +Computer.RESUME_PROMPT = 2; // automatically save but conditionally restore (WARNING: if restore is declined, any state is discarded) +Computer.RESUME_DELETE = 3; // same as RESUME_PROMPT but discards ALL machines states whenever ANY machine restore is declined (undocumented) /** * getMachineID() @@ -388,7 +400,7 @@ Computer.prototype.wait = function(fn, parms) Computer.prototype.validateState = function(stateComputer) { var fValid = true; - var stateValidate = new State(this, Computer.sAppVer, Computer.STATE_VALIDATE); + var stateValidate = new State(this, Computer.APPVERSION, Computer.STATE_VALIDATE); if (stateValidate.load() && stateValidate.parse()) { var sTimestampValidate = stateValidate.get(Computer.STATE_TIMESTAMP); var sTimestampComputer = stateComputer ? stateComputer.get(Computer.STATE_TIMESTAMP) : "unknown"; @@ -423,10 +435,15 @@ Computer.prototype.powerOn = function(resume) this.printMessage("Computer.powerOn(" + (resume == Computer.RESUME_REPOWER ? "repower" : (resume ? "resume" : "")) + ")"); } + if (this.nPowerChange) { + return; + } + this.nPowerChange++; + var fRepower = false; var fRestore = false; this.fRestoreError = false; - var stateComputer = this.stateComputer || new State(this, Computer.sAppVer); + var stateComputer = this.stateComputer || new State(this, Computer.APPVERSION); if (resume == Computer.RESUME_REPOWER) { fRepower = true; @@ -439,7 +456,7 @@ Computer.prototype.powerOn = function(resume) * Which means, of course, that if a previous "failsafe" checkpoint already exists, something bad * may have happened the last time around. */ - this.stateFailSafe = new State(this, Computer.sAppVer, Computer.STATE_FAILSAFE); + this.stateFailSafe = new State(this, Computer.APPVERSION, Computer.STATE_FAILSAFE); if (this.stateFailSafe.load()) { this.powerReport(stateComputer); /* @@ -459,7 +476,7 @@ Computer.prototype.powerOn = function(resume) this.stateFailSafe.store(); var fValidate = this.resume && !this.fServerState; - if (resume == Computer.RESUME_AUTO || web.confirmUser("Click OK to restore the previous " + Computer.sAppName + " machine state, or CANCEL to reset the machine.")) { + if (resume == Computer.RESUME_AUTO || web.confirmUser("Click OK to restore the previous " + Computer.APPNAME + " machine state, or CANCEL to reset the machine.")) { fRestore = stateComputer.parse(); if (fRestore) { var sCode = stateComputer.get(UserAPI.RES.CODE); @@ -663,9 +680,11 @@ Computer.prototype.donePowerOn = function(aParms) } this.aFlags.fPowered = true; + var controlPower = this.bindings["power"]; + if (controlPower) controlPower.textContent = "On"; if (!this.fInitialized) { - this.println(Computer.sAppName + " v" + Computer.sAppVer + "\n" + Computer.sCopyright + "\n" + Computer.LICENSE); + this.println(Computer.APPNAME + " v" + Computer.APPVERSION + "\n" + Computer.COPYRIGHT + "\n" + Computer.LICENSE); this.fInitialized = true; } @@ -694,6 +713,8 @@ Computer.prototype.donePowerOn = function(aParms) this.stateFailSafe.clear(); delete this.stateFailSafe; } + + this.nPowerChange = 0; }; /** @@ -732,8 +753,8 @@ Computer.prototype.checkPower = function() */ Computer.prototype.powerReport = function(stateComputer) { - if (web.confirmUser("There may be a problem with your " + Computer.sAppName + " machine.\n\nTo help us diagnose it, click OK to send this " + Computer.sAppName + " machine state to http://" + SITEHOST + ".")) { - web.sendReport(Computer.sAppName, Computer.sAppVer, this.url, this.getUserID(), ReportAPI.TYPE.BUG, stateComputer.toString()); + if (web.confirmUser("There may be a problem with your " + Computer.APPNAME + " machine.\n\nTo help us diagnose it, click OK to send this " + Computer.APPNAME + " machine state to http://" + SITEHOST + ".")) { + web.sendReport(Computer.APPNAME, Computer.APPVERSION, this.url, this.getUserID(), ReportAPI.TYPE.BUG, stateComputer.toString()); } }; @@ -777,8 +798,13 @@ Computer.prototype.powerOff = function(fSave, fShutdown) this.printMessage("Computer.powerOff(" + (fSave ? "save" : "nosave") + (fShutdown ? ",shutdown" : "") + ")"); } - var stateComputer = new State(this, Computer.sAppVer); - var stateValidate = new State(this, Computer.sAppVer, Computer.STATE_VALIDATE); + if (this.nPowerChange) { + return null; + } + this.nPowerChange--; + + var stateComputer = new State(this, Computer.APPVERSION); + var stateValidate = new State(this, Computer.APPVERSION, Computer.STATE_VALIDATE); var sTimestamp = usr.getTimestamp(); stateValidate.set(Computer.STATE_TIMESTAMP, sTimestamp); @@ -862,7 +888,13 @@ Computer.prototype.powerOff = function(fSave, fShutdown) } } - if (fShutdown) this.aFlags.fPowered = false; + if (fShutdown) { + this.aFlags.fPowered = false; + var controlPower = this.bindings["power"]; + if (controlPower) controlPower.textContent = "Off"; + } + + this.nPowerChange = 0; return sState; }; @@ -960,6 +992,20 @@ Computer.prototype.setBinding = function(sHTMLType, sBinding, control) var computer = this; switch (sBinding) { + case "power": + this.bindings[sBinding] = control; + control.onclick = function onClickPower() { + computer.onPower(); + }; + return true; + + case "reset": + this.bindings[sBinding] = control; + control.onclick = function onClickReset() { + computer.onReset(); + }; + return true; + case "save": this.bindings[sBinding] = control; control.onclick = function onClickSave() { @@ -996,13 +1042,6 @@ Computer.prototype.setBinding = function(sHTMLType, sBinding, control) }; return true; - case "reset": - this.bindings[sBinding] = control; - control.onclick = function onClickReset() { - computer.onReset(); - }; - return true; - default: break; } @@ -1092,7 +1131,7 @@ Computer.prototype.getServerStatePath = function() if (DEBUG && this.messageEnabled()) { this.printMessage(Computer.STATE_USERID + " for load: " + this.sUserID); } - sStatePath = web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.LOAD + '&' + UserAPI.QUERY.USER + '=' + this.sUserID + '&' + UserAPI.QUERY.STATE + '=' + State.key(this, Computer.sAppVer); + sStatePath = web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.LOAD + '&' + UserAPI.QUERY.USER + '=' + this.sUserID + '&' + UserAPI.QUERY.STATE + '=' + State.key(this, Computer.APPVERSION); } else { if (DEBUG && this.messageEnabled()) { this.printMessage(Computer.STATE_USERID + " unavailable"); @@ -1160,7 +1199,7 @@ Computer.prototype.storeServerState = function(sUserID, sState, fSync) var data = {}; data[UserAPI.QUERY.REQ] = UserAPI.REQ.STORE; data[UserAPI.QUERY.USER] = sUserID; - data[UserAPI.QUERY.STATE] = State.key(this, Computer.sAppVer); + data[UserAPI.QUERY.STATE] = State.key(this, Computer.APPVERSION); data[UserAPI.QUERY.DATA] = sState; var sRequest = web.getHost() + UserAPI.ENDPOINT; if (!fSync) { @@ -1182,13 +1221,39 @@ Computer.prototype.storeServerState = function(sUserID, sState, fSync) return null; }; +/** + * onPower() + * + * This handles UI requests to toggle the computer's power (eg, see the "power" button binding). + * + * @this {Computer} + */ +Computer.prototype.onPower = function() +{ + if (!this.nPowerChange) { + if (!this.aFlags.fPowered) { + this.wait(this.powerOn); + } else { + this.powerOff(false, true); + } + } +}; + /** * onReset() * + * This handles UI requests to reset the computer's state (eg, see the "reset" button binding). + * * @this {Computer} */ Computer.prototype.onReset = function() { + /* + * I'm going to start with the presumption that it makes little sense for an "unpowered" computer to be "reset"; + * ditto if the power state is currently being changed. + */ + if (!this.aFlags.fPowered || this.nPowerChange) return; + /* * If this is a "resumable" machine (and it's not using a predefined state), then we overload the reset * operation to offer an explicit "save or discard" option first. This is currently the only UI we offer to @@ -1203,7 +1268,7 @@ Computer.prototype.onReset = function() * I used to bypass the prompt if this.resume == Computer.RESUME_AUTO, setting fSave to true automatically, * but that gives the user no means of resetting a resumable machine that contains errors in its resume state. */ - var fSave = (/* this.resume == Computer.RESUME_AUTO || */ web.confirmUser("Click OK to save changes to this " + Computer.sAppName + " machine.\n\nWARNING: If you CANCEL, all disk changes will be discarded.")); + var fSave = (/* this.resume == Computer.RESUME_AUTO || */ web.confirmUser("Click OK to save changes to this " + Computer.APPNAME + " machine.\n\nWARNING: If you CANCEL, all disk changes will be discarded.")); this.powerOff(fSave, true); /* * Forcing the page to reload is an expedient option, but ugly. It's preferable to call powerOn() @@ -1265,7 +1330,7 @@ Computer.init = function() /* * In non-COMPILED builds, embedMachine() may have set XMLVERSION. */ - if (!COMPILED && XMLVERSION) Computer.sAppVer = XMLVERSION; + if (!COMPILED && XMLVERSION) Computer.APPVERSION = XMLVERSION; var aeMachines = Component.getElementsByClass(window.document, PCJSCLASS + "-machine"); @@ -1292,17 +1357,16 @@ Computer.init = function() } /* - * For now, all we support are "reset" and "save" buttons. We may eventually add a "power" - * button to manually suspend/resume the machine. An "erase" button was also considered, but - * "reset" now provides a way to force the machine to start from scratch again, so "erase" - * might be redundant now. + * Bind any "power", "reset" and "save" buttons. An "erase" button was also considered, + * but "reset" now provides a way to force the machine to start from scratch again, so "erase" + * may be redundant now. */ Component.bindComponentControls(computer, eComputer, PCJSCLASS); /* - * Power "up" the computer, giving every component the opportunity to reset or restore itself. + * Power on the computer, giving every component the opportunity to reset or restore itself. */ - computer.wait(computer.powerOn); + if (computer.fAutoPower) computer.wait(computer.powerOn); } } }; @@ -1380,7 +1444,7 @@ Computer.exit = function() if (computer.aFlags.fPowered) { /** - * Power "down" the computer, giving every component an opportunity to save its state, + * Power off the computer, giving every component an opportunity to save its state, * but only if 'resume' has been set AND there is no valid resume path (because if a valid resume * path exists, we'll always load our state from there, and not from whatever we save here). */ diff --git a/modules/pcjs/lib/cpu.js b/modules/pcjs/lib/cpu.js index 99deaf14f..d99f8dd92 100644 --- a/modules/pcjs/lib/cpu.js +++ b/modules/pcjs/lib/cpu.js @@ -22,7 +22,7 @@ * * You are required to include the above copyright notice in every source code file of every * copy or modified version of this work, and to display that copyright notice on every screen - * that loads or runs any version of this software (see Computer.sCopyright). + * that loads or runs any version of this software (see Computer.COPYRIGHT). * * Some PCjs files also attempt to load external resource files, such as character-image files, * ROM files, and disk image files. Those external resource files are not considered part of the @@ -167,6 +167,8 @@ CPU.YIELDS_PER_SECOND = 30; CPU.VIDEO_UPDATES_PER_SECOND = 60; // WARNING: if you change this, beware of side-effects in the Video component CPU.STATUS_UPDATES_PER_SECOND = 2; +CPU.BUTTONS = ["power", "reset"]; + /** * initBus(cmp, bus, cpu, dbg) * @@ -181,6 +183,12 @@ CPU.prototype.initBus = function(cmp, bus, cpu, dbg) this.cmp = cmp; this.bus = bus; this.dbg = dbg; + + for (var i = 0; i < CPU.BUTTONS.length; i++) { + var control = this.bindings[CPU.BUTTONS[i]]; + if (control) this.cmp.setBinding(null, CPU.BUTTONS[i], control); + } + this.fpu = cmp.getMachineComponent("FPU"); /* @@ -522,6 +530,17 @@ CPU.prototype.setBinding = function(sHTMLType, sBinding, control) var cpu = this; var fBound = false; switch (sBinding) { + case "power": + case "reset": + /* + * The "power" and "reset" buttons are functions of the entire computer, not just the CPU, + * but it's not always convenient to stick a power button in the Computer component definition, + * so we record those bindings here and pass them on to the Computer component in initBus(). + */ + this.bindings[sBinding] = control; + fBound = true; + break; + case "run": this.bindings[sBinding] = control; control.onclick = function onClickRun() { @@ -534,19 +553,6 @@ CPU.prototype.setBinding = function(sHTMLType, sBinding, control) fBound = true; break; - case "reset": - /* - * A "reset" button is really a function of the entire computer, not just the CPU, but - * it's not always convenient to stick a reset button in the computer component definition, - * so we support a "reset" binding both here AND in the Computer component. - */ - this.bindings[sBinding] = control; - control.onclick = function onClickReset() { - if (cpu.cmp) cpu.cmp.onReset(); - }; - fBound = true; - break; - case "speed": this.bindings[sBinding] = control; fBound = true;