import { Debugger } from /**/"./Debugger.js"; /////////////////////////////////////////////////////////////////////////////// // App // /////////////////////////////////////////////////////////////////////////////// // Web-based emulator application class App extends Toolkit { ///////////////////////// Initialization Methods ////////////////////////// constructor(options) { super({ className: "tk tk-app", label : "app.title", role : "application", tagName : "div", style : { display : "flex", flexDirection: "column" } }); // Configure instance fields options = options || {}; this.debugMode = true; this.dualSims = false; this.core = options.core; this.linkSims = true; this.locales = {}; this.themes = {}; this.Toolkit = Toolkit; // Configure themes if ("themes" in options) for (let theme of Object.entries(options.themes)) this.addTheme(theme[0], theme[1]); if ("theme" in options) this.setTheme(options.theme); // Configure locales if ("locales" in options) for (let locale of options.locales) this.addLocale(locale); if ("locale" in options) this.setLocale(options.locale); // Configure widget this.localize(this); // Not presenting a standalone application if (!options.standalone) return; // Set up standalone widgets this.initMenuBar(); this.desktop = new Toolkit.Desktop(this, { style: { flexGrow: 1 } }); this.add(this.desktop); // Configure document for presentation document.body.className = "tk tk-body"; window.addEventListener("resize", e=> this.element.style.height = window.innerHeight + "px"); window.dispatchEvent(new Event("resize")); document.body.appendChild(this.element); // Configure debugger components this[0] = new Debugger(this, 0, this.core[0]); this[1] = new Debugger(this, 1, this.core[1]); // Configure subscription handling this.subscriptions = { [this.core[0].sim]: this[0], [this.core[1].sim]: this[1] }; this.core.onsubscriptions = e=>this.onSubscriptions(e); // Temporary config debugging console.log("Disassembler keyboard commands:"); console.log(" Ctrl+G: Goto (also works in Memory window)"); console.log(" F10: Run to next"); console.log(" F11: Single step"); console.log("Call dasm(\"key\", value) in the console " + "to configure the disassembler:"); console.log(this[0].getDasmConfig()); window.dasm = (key, value)=>{ let config = this[0].getDasmConfig(); if (!key in config || typeof value != typeof config[key]) return; if (typeof value == "number" && value != 1 && value != 0) return; config[key] = value; this[0].setDasmConfig(config); this[1].setDasmConfig(config); return this[0].getDasmConfig(); }; } // Configure file menu initFileMenu(menuBar) { let menu, item; // Menu menuBar.add(menu = menuBar.file = new Toolkit.MenuItem(this, { text: "app.menu.file._" })); // Load ROM menu.add(item = menu.loadROM0 = new Toolkit.MenuItem(this, { text: "app.menu.file.loadROM" })); item.setSubstitution("sim", ""); item.addEventListener("action", ()=>this.promptFile(f=>this.loadROM(0, f))); menu.add(item = menu.loadROM1 = new Toolkit.MenuItem(this, { text : "app.menu.file.loadROM", visible: false })); item.setSubstitution("sim", " 2"); item.addEventListener("action", ()=>this.promptFile(f=>this.loadROM(1, f))); // Debug Mode menu.add(item = menu.debugMode = new Toolkit.MenuItem(this, { checked: this.debugMode, enabled: false, text : "app.menu.file.debugMode", type : "check" })); item.addEventListener("action", e=>e.component.setChecked(true)); } // Configure Emulation menu initEmulationMenu(menuBar) { let menu, item; menuBar.add(menu = menuBar.emulation = new Toolkit.MenuItem(this, { text: "app.menu.emulation._" })); menu.add(item = menu.runPause = new Toolkit.MenuItem(this, { enabled: false, text : "app.menu.emulation.run" })); menu.add(item = menu.reset = new Toolkit.MenuItem(this, { enabled: false, text : "app.menu.emulation.reset" })); menu.add(item = menu.dualSims = new Toolkit.MenuItem(this, { checked: this.dualSims, text : "app.menu.emulation.dualSims", type : "check" })); item.addEventListener("action", e=>this.setDualSims(e.component.isChecked)); menu.add(item = menu.linkSims = new Toolkit.MenuItem(this, { checked: this.linkSims, text : "app.menu.emulation.linkSims", type : "check", visible: this.dualSims })); item.addEventListener("action", e=>this.setLinkSims(e.component.isChecked)); } // Configure Debug menus initDebugMenu(menuBar, sim) { let menu, item; menuBar.add(menu = menuBar["debug" + sim] = new Toolkit.MenuItem(this, { text : "app.menu.debug._", visible: sim == 0 || this.dualSims })); menu.setSubstitution("sim", sim == 1 || this.dualSims ? " " + (sim + 1) : ""); menu.add(item = menu.console = new Toolkit.MenuItem(this, { text: "app.menu.debug.console", enabled: false })); menu.add(item = menu.memory = new Toolkit.MenuItem(this, { text: "app.menu.debug.memory" })); item.addEventListener("action", ()=>this.showWindow(this[sim].memoryWindow)); menu.add(item = menu.cpu = new Toolkit.MenuItem(this, { text: "app.menu.debug.cpu" })); item.addEventListener("action", ()=>this.showWindow(this[sim].cpuWindow)); menu.add(item = menu.breakpoints = new Toolkit.MenuItem(this, { text: "app.menu.debug.breakpoints", enabled: false })); menu.addSeparator(); menu.add(item = menu.palettes = new Toolkit.MenuItem(this, { text: "app.menu.debug.palettes", enabled: false })); menu.add(item = menu.characters = new Toolkit.MenuItem(this, { text: "app.menu.debug.characters", enabled: false })); menu.add(item = menu.bgMaps = new Toolkit.MenuItem(this, { text: "app.menu.debug.bgMaps", enabled: false })); menu.add(item = menu.objects = new Toolkit.MenuItem(this, { text: "app.menu.debug.objects", enabled: false })); menu.add(item = menu.worlds = new Toolkit.MenuItem(this, { text: "app.menu.debug.worlds", enabled: false })); menu.add(item = menu.frameBuffers = new Toolkit.MenuItem(this, { text: "app.menu.debug.frameBuffers", enabled: false })); } // Configure Theme menu initThemeMenu(menuBar) { let menu, item; menuBar.add(menu = menuBar.theme = new Toolkit.MenuItem(this, { text: "app.menu.theme._" })); menu.add(item = menu.light = new Toolkit.MenuItem(this, { text: "app.menu.theme.light" })); item.addEventListener("action", e=>this.setTheme("light")); menu.add(item = menu.dark = new Toolkit.MenuItem(this, { text: "app.menu.theme.dark" })); item.addEventListener("action", e=>this.setTheme("dark")); menu.add(item = menu.virtual = new Toolkit.MenuItem(this, { text: "app.menu.theme.virtual" })); item.addEventListener("action", e=>this.setTheme("virtual")); } // Set up the menu bar initMenuBar() { let menuBar = this.menuBar = new Toolkit.MenuBar(this, { label: "app.menu._" }); this.initFileMenu (menuBar); this.initEmulationMenu(menuBar); this.initDebugMenu (menuBar, 0); this.initDebugMenu (menuBar, 1); this.initThemeMenu (menuBar); this.add(menuBar); } ///////////////////////////// Event Handlers ////////////////////////////// // Subscriptions arrived from the core thread onSubscriptions(subscriptions) { for (let sim of Object.entries(subscriptions)) { let dbg = this.subscriptions[sim[0]]; for (let sub of Object.entries(sim[1])) switch (sub[0]) { case "proregs": dbg.programRegisters.refresh(sub[1]); break; case "sysregs": dbg.systemRegisters .refresh(sub[1]); break; case "dasm" : dbg.disassembler .refresh(sub[1]); break; case "memory" : dbg.memory .refresh(sub[1]); break; } } } ///////////////////////////// Public Methods ////////////////////////////// // Register a locale JSON addLocale(locale) { if (!("id" in locale)) throw "No id field in locale"; this.locales[locale.id] = Toolkit.flatten(locale); } // Register a theme stylesheet addTheme(id, stylesheet) { this.themes[id] = stylesheet; } ///////////////////////////// Package Methods ///////////////////////////// // Specify the language for localization management setLocale(id) { if (!(id in this.locales)) { let lang = id.substring(0, 2); id = "en-US"; for (let key of Object.keys(this.locales)) { if (key.substring(0, 2) == lang) { id = key; break; } } } super.setLocale(this.locales[id]); } // Specify the active color theme setTheme(key) { if (!(key in this.themes)) return; for (let tkey of Object.keys(this.themes)) this.themes[tkey].setEnabled(tkey == key); } // Regenerate localized display text translate() { if (arguments.length != 0) return super.translate.apply(this, arguments); document.title = super.translate("app.title", this); } ///////////////////////////// Private Methods ///////////////////////////// // Load a ROM for a simulation async loadROM(index, file) { // No file was given if (!file) return; // Load the file into memory try { file = new Uint8Array(await file.arrayBuffer()); } catch { alert(this.translate("error.fileRead")); return; } // Validate file size if ( file.length < 1024 || file.length > 0x1000000 || (file.length - 1 & file.length) != 0 ) { alert(this.translate("error.romNotVB")); return; } // Load the ROM into the simulation if (!(await this[index].sim.setROM(file, { refresh: true }))) { alert(this.translate("error.romNotVB")); return; } // Seek the disassembler to PC this[index].disassembler.seek(0xFFFFFFF0, true); } // Prompt the user to select a file promptFile(then) { let file = document.createElement("input"); file.type = "file"; file.addEventListener("input", e=>file.files[0] && then(file.files[0])); file.click(); } // Attempt to run until the next instruction async runNext(index) { let two = this.dualSims && this.linkSims; // Perform the operation let data = await this.core.runNext( this[index].sim.sim, two ? this[index ^ 1].sim.sim : 0, { refresh: true }); // Update the disassemblers this[index].disassembler.pc = data.pc[0]; this[index].disassembler.seek(data.pc[0]); if (two) { this[index ^ 1].disassembler.pc = data.pc[1]; this[index ^ 1].disassembler.seek(data.pc[1]); } } // Specify whether dual sims mode is active setDualSims(dualSims) { let sub = dualSims ? " 1" : ""; // Configure instance fields this.dualSims = dualSims = !!dualSims; // Configure menus this.menuBar.emulation.dualSims.setChecked(dualSims); this.menuBar.emulation.linkSims.setVisible(dualSims); this.menuBar.file.loadROM0.setSubstitution("sim", sub); this.menuBar.file.loadROM1.setVisible(dualSims); this.menuBar.debug0.setSubstitution("sim", sub); this.menuBar.debug1.setVisible(dualSims); // Configure debuggers this[0].setDualSims(dualSims); this[1].setDualSims(dualSims); this.core.connect(this[0].sim.sim, dualSims && this.linkSims ? this[1].sim.sim : 0); } // Specify whether the sims are connected for communicatinos setLinkSims(linked) { linked = !!linked; // State is not changing if (linked == this.linkSims) return; // Link or un-link the sims if (this.dualSims) this.core.connect(this[0].sim.sim, linked ? this[1].sim.sim : 0); } // Display a window showWindow(wnd) { wnd.setVisible(true); wnd.focus() } // Execute one instruction async singleStep(index) { let two = this.dualSims && this.linkSims; // Perform the operation let data = await this.core.singleStep( this[index].sim.sim, two ? this[index ^ 1].sim.sim : 0, { refresh: true }); // Update the disassemblers this[index].disassembler.pc = data.pc[0]; this[index].disassembler.seek(data.pc[0]); if (two) { this[index ^ 1].disassembler.pc = data.pc[1]; this[index ^ 1].disassembler.seek(data.pc[1]); } } } export { App };