Full front-end rewrite
This commit is contained in:
parent
fccb799a9c
commit
3cf006ba13
184
app/App.js
184
app/App.js
|
@ -1,184 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Top-level state and UI manager
|
||||
globalThis.App = class App {
|
||||
|
||||
// Produce a new class instance
|
||||
static async create() {
|
||||
let ret = new App();
|
||||
await ret.init();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Perform initial tasks
|
||||
async init() {
|
||||
|
||||
// Initialization tasks
|
||||
this.initEnv();
|
||||
this.initMenus();
|
||||
|
||||
// WebAssembly core module
|
||||
this.core = new Worker(Bundle.get("app/Emulator.js").toDataURL());
|
||||
await new Promise((resolve,reject)=>{
|
||||
this.core.onmessage = e=>{
|
||||
this.core.onmessage = e=>this.onmessage(e.data);
|
||||
resolve();
|
||||
};
|
||||
this.core.postMessage({
|
||||
command: "Init",
|
||||
wasm : Bundle.get("core.wasm").data.buffer
|
||||
});
|
||||
});
|
||||
|
||||
// Desktop pane
|
||||
this.desktop = this.gui.newPanel({ layout: "desktop" });
|
||||
this.desktop.setRole("group");
|
||||
this.desktop.element.setAttribute("desktop", "");
|
||||
this.gui.add(this.desktop);
|
||||
|
||||
// Debugging windows
|
||||
this.debuggers = [
|
||||
new Debugger(this, 0),
|
||||
new Debugger(this, 1)
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
// Initialize environment settings
|
||||
initEnv() {
|
||||
|
||||
// Configure themes
|
||||
Bundle.get("app/theme/kiosk.css").style();
|
||||
this.themes = {
|
||||
dark : Bundle.get("app/theme/dark.css" ).style(false),
|
||||
light : Bundle.get("app/theme/light.css" ).style(true ),
|
||||
virtual: Bundle.get("app/theme/virtual.css").style(false)
|
||||
};
|
||||
this.theme = this.themes["light"];
|
||||
|
||||
// Produce toolkit instance
|
||||
this.gui = new Toolkit.Application({
|
||||
layout: "grid",
|
||||
rows : "max-content auto"
|
||||
});
|
||||
document.body.appendChild(this.gui.element);
|
||||
window.addEventListener("resize", ()=>{
|
||||
this.gui.setSize(window.innerWidth+"px", window.innerHeight+"px");
|
||||
});
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
|
||||
// Configure locales
|
||||
this.gui.addLocale(Bundle.get("app/locale/en-US.js").toString());
|
||||
this.gui.setLocale(navigator.language);
|
||||
}
|
||||
|
||||
// Initialize main menu bar
|
||||
initMenus() {
|
||||
|
||||
// Menu bar
|
||||
this.mainMenu = this.gui.newMenuBar({ name: "{menu._}" });
|
||||
this.gui.add(this.mainMenu);
|
||||
this.gui.addPropagationListener(e=>this.mainMenu.restoreFocus());
|
||||
|
||||
// File menu
|
||||
let menu = this.mainMenu.newMenu({ text: "{menu.file._}"});
|
||||
let item = menu.newMenuItem({ text: "{menu.file.loadROM}"});
|
||||
item.addClickListener(()=>this.loadROM());
|
||||
|
||||
// Debug menu
|
||||
menu = this.mainMenu.newMenu({ text: "{menu.debug._}" });
|
||||
item = menu.newMenuItem({ text: "{memory._}" });
|
||||
item.addClickListener(
|
||||
()=>this.debuggers[0].memory.setVisible(true, true));
|
||||
item = menu.newMenuItem({ text: "{cpu._}" });
|
||||
item.addClickListener(
|
||||
()=>this.debuggers[0].cpu.setVisible(true, true));
|
||||
|
||||
// Theme menu
|
||||
menu = this.mainMenu.newMenu({ text: "{menu.theme._}" });
|
||||
item = menu.newMenuItem({ text: "{menu.theme.light}" });
|
||||
item.addClickListener(()=>this.setTheme("light"));
|
||||
item = menu.newMenuItem({ text: "{menu.theme.dark}" });
|
||||
item.addClickListener(()=>this.setTheme("dark"));
|
||||
item = menu.newMenuItem({ text: "{menu.theme.virtual}" });
|
||||
item.addClickListener(()=>this.setTheme("virtual"));
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////// Message Methods /////////////////////////////
|
||||
|
||||
// Message received
|
||||
onmessage(msg) {
|
||||
if ("dbgwnd" in msg) {
|
||||
this.debuggers[msg.sim].message(msg);
|
||||
return;
|
||||
}
|
||||
switch (msg.command) {
|
||||
case "SetROM": this.setROM(msg); break;
|
||||
}
|
||||
}
|
||||
|
||||
// ROM buffer has been configured
|
||||
setROM(msg) {
|
||||
let dbg = this.debuggers[msg.sim];
|
||||
dbg.refresh(true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Prompt the user to select a ROM file
|
||||
loadROM() {
|
||||
let file = document.createElement("input");
|
||||
file.type = "file";
|
||||
file.addEventListener("input", ()=>this.selectROM(file.files[0]));
|
||||
file.click();
|
||||
}
|
||||
|
||||
// Specify a ROM file
|
||||
async selectROM(file) {
|
||||
|
||||
// No file is specified (perhaps the user canceled)
|
||||
if (file == null)
|
||||
return;
|
||||
|
||||
// Check the file's size
|
||||
if (
|
||||
file.size < 1024 ||
|
||||
file.size > 0x1000000 ||
|
||||
(file.size - 1 & file.size) != 0
|
||||
) {
|
||||
alert(this.gui.translate("{app.romNotVB}"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the file data into a byte buffer
|
||||
let filename = file.name;
|
||||
try { file = await file.arrayBuffer(); }
|
||||
catch {
|
||||
alert(this.gui.translate("{app.readFileError}"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the ROM to the WebAssembly core module
|
||||
this.core.postMessage({
|
||||
command: "SetROM",
|
||||
reset : true,
|
||||
rom : file,
|
||||
sim : 0
|
||||
}, file);
|
||||
}
|
||||
|
||||
// Specify the current color theme
|
||||
setTheme(key) {
|
||||
let theme = this.themes[key];
|
||||
if (theme == this.theme)
|
||||
return;
|
||||
let old = this.theme;
|
||||
this.theme = theme;
|
||||
theme.setEnabled(true);
|
||||
old.setEnabled(false);
|
||||
}
|
||||
|
||||
};
|
|
@ -98,12 +98,12 @@ public class Bundle {
|
|||
var manifest = new StringBuilder();
|
||||
manifest.append("\"use strict\";\nlet manifest=[");
|
||||
for (var file : values) {
|
||||
manifest.append("{" +
|
||||
"name:\"" + file.filename + "\"," +
|
||||
"size:" + file.data.length +
|
||||
"},");
|
||||
manifest.append(
|
||||
"[\"" + file.filename + "\"," + file.data.length + "]");
|
||||
if (file != values[values.length - 1])
|
||||
manifest.append(",");
|
||||
}
|
||||
manifest.append("];\nlet bundleName=\"" + bundleName + "\";\n");
|
||||
manifest.append("],bundleName=\"" + bundleName + "\";");
|
||||
|
||||
// Prepend the manifest to _boot.js
|
||||
var boot = files.get("app/_boot.js");
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Debugging UI manager
|
||||
globalThis.Debugger = class Debugger {
|
||||
|
||||
// Object constructor
|
||||
constructor(app, sim) {
|
||||
|
||||
// Configure instance fields
|
||||
this.app = app;
|
||||
this.core = app.core;
|
||||
this.gui = app.gui;
|
||||
this.sim = sim;
|
||||
|
||||
// Memory window
|
||||
this.memory = new MemoryWindow(this, {
|
||||
title : "{sim}{memory._}",
|
||||
height : 300,
|
||||
visible: false,
|
||||
width : 400
|
||||
});
|
||||
this.memory.addCloseListener(e=>this.memory.setVisible(false));
|
||||
app.desktop.add(this.memory);
|
||||
|
||||
// CPU window
|
||||
this.cpu = new CPUWindow(this, {
|
||||
title : "{sim}{cpu._}",
|
||||
height : 300,
|
||||
visible: false,
|
||||
width : 400
|
||||
});
|
||||
this.cpu.addCloseListener(e=>this.cpu.setVisible(false));
|
||||
app.desktop.add(this.cpu);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Message received from emulation thread
|
||||
message(msg) {
|
||||
switch (msg.dbgwnd) {
|
||||
case "CPU" : this.cpu .message(msg); break;
|
||||
case "Memory": this.memory.message(msg); break;
|
||||
}
|
||||
}
|
||||
|
||||
// Reload all output
|
||||
refresh(seekToPC) {
|
||||
this.cpu .refresh(seekToPC);
|
||||
this.memory.refresh();
|
||||
}
|
||||
|
||||
};
|
161
app/Emulator.js
161
app/Emulator.js
|
@ -1,161 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Worker that manages a WebAssembly instance of the C core library
|
||||
(globalThis.Emulator = class Emulator {
|
||||
|
||||
// Static initializer
|
||||
static initializer() {
|
||||
new Emulator();
|
||||
}
|
||||
|
||||
// Object constructor
|
||||
constructor() {
|
||||
|
||||
// Configure instance fields
|
||||
this.buffers = {};
|
||||
|
||||
// Configure message port
|
||||
onmessage = e=>this.onmessage(e.data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Message Methods /////////////////////////////
|
||||
|
||||
// Message received
|
||||
onmessage(msg) {
|
||||
switch (msg.command) {
|
||||
case "GetRegisters": this.getRegisters(msg); break;
|
||||
case "Init" : this.init (msg); break;
|
||||
case "ReadBuffer" : this.readBuffer (msg); break;
|
||||
case "RunNext" : this.runNext (msg); break;
|
||||
case "SetRegister" : this.setRegister (msg); break;
|
||||
case "SetROM" : this.setROM (msg); break;
|
||||
case "SingleStep" : this.singleStep (msg); break;
|
||||
case "Write" : this.write (msg); break;
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the values of all the CPU registers
|
||||
getRegisters(msg) {
|
||||
msg.pc = this.core.GetProgramCounter(msg.sim, 0) >>> 0;
|
||||
msg.pcFrom = this.core.GetProgramCounter(msg.sim, 1) >>> 0;
|
||||
msg.pcTo = this.core.GetProgramCounter(msg.sim, 2) >>> 0;
|
||||
msg.adtre = this.core.GetSystemRegister(msg.sim, 25) >>> 0;
|
||||
msg.chcw = this.core.GetSystemRegister(msg.sim, 24) >>> 0;
|
||||
msg.ecr = this.core.GetSystemRegister(msg.sim, 4) >>> 0;
|
||||
msg.eipc = this.core.GetSystemRegister(msg.sim, 0) >>> 0;
|
||||
msg.eipsw = this.core.GetSystemRegister(msg.sim, 1) >>> 0;
|
||||
msg.fepc = this.core.GetSystemRegister(msg.sim, 2) >>> 0;
|
||||
msg.fepsw = this.core.GetSystemRegister(msg.sim, 3) >>> 0;
|
||||
msg.pir = this.core.GetSystemRegister(msg.sim, 6) >>> 0;
|
||||
msg.psw = this.core.GetSystemRegister(msg.sim, 5) >>> 0;
|
||||
msg.tkcw = this.core.GetSystemRegister(msg.sim, 7) >>> 0;
|
||||
msg.sr29 = this.core.GetSystemRegister(msg.sim, 29) >>> 0;
|
||||
msg.sr30 = this.core.GetSystemRegister(msg.sim, 30) >>> 0;
|
||||
msg.sr31 = this.core.GetSystemRegister(msg.sim, 31) >>> 0;
|
||||
msg.program = new Array(32);
|
||||
for (let x = 0; x <= 31; x++)
|
||||
msg.program[x] = this.core.GetProgramRegister(msg.sim, x);
|
||||
postMessage(msg);
|
||||
}
|
||||
|
||||
// Initialize the WebAssembly core module
|
||||
async init(msg) {
|
||||
|
||||
// Load and instantiate the WebAssembly module
|
||||
this.wasm = await WebAssembly.instantiate(msg.wasm,
|
||||
{ env: { emscripten_notify_memory_growth: ()=>this.onmemory() }});
|
||||
this.wasm.instance.exports.Init();
|
||||
|
||||
// Configure instance fields
|
||||
this.core = this.wasm.instance.exports;
|
||||
|
||||
postMessage({ command: "Init" });
|
||||
}
|
||||
|
||||
// Read multiple data units from the bus
|
||||
readBuffer(msg) {
|
||||
let buffer = this.malloc(Uint8Array, msg.size);
|
||||
this.core.ReadBuffer(msg.sim, buffer.pointer,
|
||||
msg.address, msg.size, msg.debug ? 1 : 0);
|
||||
msg.buffer = this.core.memory.buffer.slice(
|
||||
buffer.pointer, buffer.pointer + msg.size);
|
||||
this.free(buffer);
|
||||
msg.pc = this.core.GetProgramCounter(msg.sim) >>> 0;
|
||||
postMessage(msg, msg.buffer);
|
||||
}
|
||||
|
||||
// Attempt to advance to the next instruction
|
||||
runNext(msg) {
|
||||
this.core.RunNext(msg.sim);
|
||||
postMessage(msg);
|
||||
}
|
||||
|
||||
// Specify a new value for a register
|
||||
setRegister(msg) {
|
||||
switch (msg.type) {
|
||||
case "pc" : msg.value =
|
||||
this.core.SetProgramCounter (msg.sim, msg.value);
|
||||
break;
|
||||
case "program": msg.value =
|
||||
this.core.SetProgramRegister(msg.sim, msg.id, msg.value);
|
||||
break;
|
||||
case "system" : msg.value =
|
||||
this.core.SetSystemRegister (msg.sim, msg.id, msg.value);
|
||||
}
|
||||
postMessage(msg);
|
||||
}
|
||||
|
||||
// Supply a ROM buffer
|
||||
setROM(msg) {
|
||||
let rom = new Uint8Array(msg.rom);
|
||||
let buffer = this.malloc(Uint8Array, rom.length);
|
||||
for (let x = 0; x < rom.length; x++)
|
||||
buffer.data[x] = rom[x];
|
||||
msg.success = !!this.core.SetROM(msg.sim, buffer.pointer, rom.length);
|
||||
delete msg.rom;
|
||||
if (msg.reset)
|
||||
this.core.Reset(msg.sim);
|
||||
postMessage(msg);
|
||||
}
|
||||
|
||||
// Execute the current instruction
|
||||
singleStep(msg) {
|
||||
this.core.SingleStep(msg.sim);
|
||||
postMessage(msg);
|
||||
}
|
||||
|
||||
// Write a single value into the bus
|
||||
write(msg) {
|
||||
this.core.Write(msg.sim, msg.address, msg.type, msg.value,
|
||||
msg.debug ? 1 : 0);
|
||||
postMessage(msg);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Delete a previously-allocated memory buffer
|
||||
free(buffer) {
|
||||
this.core.Free(buffer.pointer);
|
||||
delete this.buffers[buffer.pointer];
|
||||
}
|
||||
|
||||
// Allocate a typed array in WebAssembly core memory
|
||||
malloc(type, size) {
|
||||
let pointer = this.core.Malloc(size);
|
||||
let data = new type(this.core.memory.buffer, pointer, size);
|
||||
return this.buffers[pointer] = { data: data, pointer: pointer };
|
||||
}
|
||||
|
||||
// WebAssembly memory has grown
|
||||
onmemory() {
|
||||
for (let buffer of Object.values(this.buffers)) {
|
||||
buffer.data = new buffer.data.constructor(
|
||||
this.core.memory.buffer, buffer.pointer);
|
||||
}
|
||||
}
|
||||
|
||||
}).initializer();
|
514
app/_boot.js
514
app/_boot.js
|
@ -1,10 +1,8 @@
|
|||
// Produce an async function from a source string
|
||||
if (!globalThis.AsyncFunction)
|
||||
globalThis.AsyncFunction =
|
||||
Object.getPrototypeOf(async function(){}).constructor;
|
||||
|
||||
// Read scripts from files on disk to aid with debugging
|
||||
let debug = location.hash == "#debug";
|
||||
/*
|
||||
The Bundle.java utility prepends a file manifest to this script before
|
||||
execution is started. This script runs within the context of an async
|
||||
function.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
@ -12,55 +10,145 @@ let debug = location.hash == "#debug";
|
|||
// Bundle //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Resource asset manager
|
||||
globalThis.Bundle = class BundledFile {
|
||||
// Bundled file manager
|
||||
let Bundle = globalThis.Bundle = new class Bundle extends Array {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Configure instance fields
|
||||
this.debug =
|
||||
location.protocol != "file:" && location.hash == "#debug";
|
||||
this.decoder = new TextDecoder();
|
||||
this.encoder = new TextEncoder();
|
||||
this.moduleCall = (... a)=>this.module (... a);
|
||||
this.resourceCall = (... a)=>this.resource(... a);
|
||||
|
||||
///////////////////////////// Static Methods //////////////////////////////
|
||||
// Generate the CRC32 lookup table
|
||||
this.crcLookup = new Uint32Array(256);
|
||||
for (let x = 0; x <= 255; x++) {
|
||||
let l = x;
|
||||
for (let j = 7; j >= 0; j--)
|
||||
l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1)));
|
||||
this.crcLookup[x] = l;
|
||||
}
|
||||
|
||||
// Adds a bundled file from loaded file data
|
||||
static add(name, data) {
|
||||
return Bundle.files[name] = new Bundle(name, data);
|
||||
}
|
||||
|
||||
// Retrieve the file given its filename
|
||||
static get(name) {
|
||||
return Bundle.files[name];
|
||||
}
|
||||
|
||||
// Run a file as a JavaScript source file
|
||||
static async run(name) {
|
||||
await Bundle.files[name].run();
|
||||
}
|
||||
|
||||
// Resolve a URL for a source file
|
||||
static source(name) {
|
||||
return debug ? name : Bundle.files[name].toDataURL();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a file to the bundle
|
||||
add(name, data) {
|
||||
this.push(this[name] = new BundledFile(name, data));
|
||||
}
|
||||
|
||||
// Export all bundled resources to a .ZIP file
|
||||
save() {
|
||||
let centrals = new Array(this.length);
|
||||
let locals = new Array(this.length);
|
||||
let offset = 0;
|
||||
let size = 0;
|
||||
|
||||
// Encode file and directory entries
|
||||
for (let x = 0; x < this.length; x++) {
|
||||
let file = this[x];
|
||||
let sum = this.crc32(file.data);
|
||||
locals [x] = file.toZipHeader(sum);
|
||||
centrals[x] = file.toZipHeader(sum, offset);
|
||||
offset += locals [x].length;
|
||||
size += centrals[x].length;
|
||||
}
|
||||
|
||||
// Encode end of central directory
|
||||
let end = [];
|
||||
this.writeInt(end, 4, 0x06054B50); // Signature
|
||||
this.writeInt(end, 2, 0); // Disk number
|
||||
this.writeInt(end, 2, 0); // Central dir start disk
|
||||
this.writeInt(end, 2, this.length); // # central dir this disk
|
||||
this.writeInt(end, 2, this.length); // # central dir total
|
||||
this.writeInt(end, 4, size); // Size of central dir
|
||||
this.writeInt(end, 4, offset); // Offset of central dir
|
||||
this.writeInt(end, 2, 0); // .ZIP comment length
|
||||
|
||||
// Prompt the user to save the resulting file
|
||||
let a = document.createElement("a");
|
||||
a.download = bundleName + ".zip";
|
||||
a.href = URL.createObjectURL(new Blob(
|
||||
locals.concat(centrals).concat([Uint8Array.from(end)]),
|
||||
{ type: "application/zip" }
|
||||
));
|
||||
a.style.visibility = "hidden";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Write a byte array into an output buffer
|
||||
writeBytes(data, bytes) {
|
||||
//data.push(... bytes);
|
||||
for (let b of bytes)
|
||||
data.push(b);
|
||||
}
|
||||
|
||||
// Write an integer into an output buffer
|
||||
writeInt(data, size, value) {
|
||||
for (; size > 0; size--, value >>= 8)
|
||||
data.push(value & 0xFF);
|
||||
}
|
||||
|
||||
// Write a string of text as bytes into an output buffer
|
||||
writeString(data, text) {
|
||||
data.push(... this.encoder.encode(text));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Calculate the CRC32 checksum for a byte array
|
||||
crc32(data) {
|
||||
let c = 0xFFFFFFFF;
|
||||
for (let x = 0; x < data.length; x++)
|
||||
c = ((c >>> 8) ^ this.crcLookup[(c ^ data[x]) & 0xFF]);
|
||||
return ~c & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
}();
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// BundledFile //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Individual bundled file
|
||||
class BundledFile {
|
||||
|
||||
// Object constructor
|
||||
constructor(name, data) {
|
||||
|
||||
// Configure instance fields
|
||||
this.data = data;
|
||||
this.name = name;
|
||||
|
||||
// Detect the MIME type
|
||||
// Resolve the MIME type
|
||||
this.mime =
|
||||
name.endsWith(".css" ) ? "text/css;charset=UTF-8" :
|
||||
name.endsWith(".frag" ) ? "text/plain;charset=UTF-8" :
|
||||
name.endsWith(".js" ) ? "text/javascript;charset=UTF-8" :
|
||||
name.endsWith(".png" ) ? "image/png" :
|
||||
name.endsWith(".svg" ) ? "image/svg+xml;charset=UTF-8" :
|
||||
name.endsWith(".txt" ) ? "text/plain;charset=UTF-8" :
|
||||
name.endsWith(".vert" ) ? "text/plain;charset=UTF-8" :
|
||||
name.endsWith(".wasm" ) ? "application/wasm" :
|
||||
name.endsWith(".woff2") ? "font/woff2" :
|
||||
name.endsWith(".css" ) ? "text/css;charset=UTF-8" :
|
||||
name.endsWith(".frag" ) ? "text/plain;charset=UTF-8" :
|
||||
name.endsWith(".js" ) ? "text/javascript;charset=UTF-8" :
|
||||
name.endsWith(".json" ) ? "application/json;charset=UTF-8" :
|
||||
name.endsWith(".png" ) ? "image/png" :
|
||||
name.endsWith(".svg" ) ? "image/svg+xml;charset=UTF-8" :
|
||||
name.endsWith(".txt" ) ? "text/plain;charset=UTF-8" :
|
||||
name.endsWith(".vert" ) ? "text/plain;charset=UTF-8" :
|
||||
name.endsWith(".wasm" ) ? "application/wasm" :
|
||||
name.endsWith(".woff2") ? "font/woff2" :
|
||||
"application/octet-stream"
|
||||
;
|
||||
|
||||
|
@ -70,255 +158,151 @@ globalThis.Bundle = class BundledFile {
|
|||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Execute the file as a JavaScript source file
|
||||
async run() {
|
||||
|
||||
// Not running in debug mode
|
||||
if (!debug) {
|
||||
await new AsyncFunction(this.toString())();
|
||||
return;
|
||||
// Install a font from a bundled resource
|
||||
async installFont(name) {
|
||||
if (name === undefined) {
|
||||
name = "/" + this.name;
|
||||
name = name.substring(name.lastIndexOf("/") + 1);
|
||||
}
|
||||
|
||||
// Running in debug mode
|
||||
await new Promise((resolve,reject)=>{
|
||||
let script = document.createElement("script");
|
||||
document.head.appendChild(script);
|
||||
script.addEventListener("load", ()=>resolve());
|
||||
script.src = this.name;
|
||||
});
|
||||
|
||||
let ret = new FontFace(name, "url('"+
|
||||
(Bundle.debug ? this.name : this.toDataURL()) + "'");
|
||||
await ret.load();
|
||||
document.fonts.add(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Register the file as a CSS stylesheet
|
||||
style(enabled) {
|
||||
let link = document.createElement("link");
|
||||
link.href = debug ? this.name : this.toDataURL();
|
||||
link.rel = "stylesheet";
|
||||
link.type = "text/css";
|
||||
link.setEnabled = enabled=>{
|
||||
// Install an image as a CSS icon from a bundled resource
|
||||
installImage(name, filename) {
|
||||
document.documentElement.style.setProperty("--" +
|
||||
name || this.name.replaceAll(/\/\./, "_"),
|
||||
"url('" + (Bundle.debug ?
|
||||
filename || this.name : this.toDataURL()) + "')");
|
||||
return name;
|
||||
}
|
||||
|
||||
// Install a stylesheet from a bundled resource
|
||||
installStylesheet(enabled) {
|
||||
let ret = document.createElement("link");
|
||||
ret.href = Bundle.debug ? this.name : this.toDataURL();
|
||||
ret.rel = "stylesheet";
|
||||
ret.type = "text/css";
|
||||
ret.setEnabled = enabled=>{
|
||||
if (enabled)
|
||||
link.removeAttribute("disabled");
|
||||
else link.setAttribute("disabled", null);
|
||||
ret.removeAttribute("disabled");
|
||||
else ret.setAttribute("disabled", "");
|
||||
};
|
||||
link.setEnabled(enabled === undefined || !!enabled);
|
||||
document.head.appendChild(link);
|
||||
return link;
|
||||
}
|
||||
|
||||
// Produce a blob from the file data
|
||||
toBlob() {
|
||||
return new Blob(this.data, { type: this.mime });
|
||||
}
|
||||
|
||||
// Produce a blob URL for the file data
|
||||
toBlobURL() {
|
||||
return URL.createObjectURL(this.toBlob());
|
||||
ret.setEnabled(!!enabled);
|
||||
document.head.appendChild(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Encode the file data as a data URL
|
||||
toDataURL() {
|
||||
return "data:" + this.mime + ";base64," + btoa(this.toString());
|
||||
return "data:" + this.mime + ";base64," +
|
||||
btoa(String.fromCharCode(...this.data));
|
||||
}
|
||||
|
||||
// Interpret the file's contents as bundled script source data URL
|
||||
toScript() {
|
||||
|
||||
// Process all URL strings prefixed with /**/
|
||||
let parts = this.toString().split("/**/");
|
||||
let src = parts.shift();
|
||||
for (let part of parts) {
|
||||
let quote = part.indexOf("\"", 1);
|
||||
|
||||
// Begin with the path of the current file
|
||||
let path = this.name.split("/");
|
||||
path.pop();
|
||||
|
||||
// Navigate to the path of the target file
|
||||
let file = part.substring(1, quote).split("/");
|
||||
while (file.length > 0) {
|
||||
let sub = file.shift();
|
||||
switch (sub) {
|
||||
case "..": path.pop(); // Fallthrough
|
||||
case "." : break;
|
||||
default : path.push(sub);
|
||||
}
|
||||
}
|
||||
|
||||
// Append the file as a data URL
|
||||
file = Bundle[path.join("/")];
|
||||
src += "\"" + file[
|
||||
file.mime.startsWith("text/javascript") ?
|
||||
"toScript" : "toDataURL"
|
||||
]() + "\"" + part.substring(quote + 1);
|
||||
}
|
||||
|
||||
// Encode the transformed source as a data URL
|
||||
return "data:" + this.mime + ";base64," + btoa(src);
|
||||
}
|
||||
|
||||
// Decode the file data as a UTF-8 string
|
||||
toString() {
|
||||
return new TextDecoder().decode(this.data);
|
||||
return Bundle.decoder.decode(this.data);
|
||||
}
|
||||
|
||||
};
|
||||
Bundle.files = [];
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Produce a .ZIP header for export
|
||||
toZipHeader(crc32, offset) {
|
||||
let central = offset !== undefined;
|
||||
let ret = [];
|
||||
if (central) {
|
||||
Bundle.writeInt (ret, 4, 0x02014B50); // Signature
|
||||
Bundle.writeInt (ret, 2, 20); // Version created by
|
||||
} else
|
||||
Bundle.writeInt (ret, 4, 0x04034B50); // Signature
|
||||
Bundle.writeInt (ret, 2, 20); // Version required
|
||||
Bundle.writeInt (ret, 2, 0); // Bit flags
|
||||
Bundle.writeInt (ret, 2, 0); // Compression method
|
||||
Bundle.writeInt (ret, 2, 0); // Modified time
|
||||
Bundle.writeInt (ret, 2, 0); // Modified date
|
||||
Bundle.writeInt (ret, 4, crc32); // Checksum
|
||||
Bundle.writeInt (ret, 4, this.data.length); // Compressed size
|
||||
Bundle.writeInt (ret, 4, this.data.length); // Uncompressed size
|
||||
Bundle.writeInt (ret, 2, this.name.length); // Filename length
|
||||
Bundle.writeInt (ret, 2, 0); // Extra field length
|
||||
if (central) {
|
||||
Bundle.writeInt (ret, 2, 0); // File comment length
|
||||
Bundle.writeInt (ret, 2, 0); // Disk number start
|
||||
Bundle.writeInt (ret, 2, 0); // Internal attributes
|
||||
Bundle.writeInt (ret, 4, 0); // External attributes
|
||||
Bundle.writeInt (ret, 4, offset); // Relative offset
|
||||
}
|
||||
Bundle.writeString (ret, this.name); // Filename
|
||||
if (!central)
|
||||
Bundle.writeBytes(ret, this.data); // File data
|
||||
return Uint8Array.from(ret);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// ZIP Bundler //
|
||||
// Boot Program //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Data buffer utility processor
|
||||
class Bin {
|
||||
|
||||
// Object constructor
|
||||
constructor() {
|
||||
this.data = [];
|
||||
this.offset = 0;
|
||||
}
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Convert the data contents to a byte array
|
||||
toByteArray() {
|
||||
return Uint8Array.from(this.data);
|
||||
}
|
||||
|
||||
// Encode a byte array
|
||||
writeBytes(data) {
|
||||
this.data = this.data.concat(Array.from(data));
|
||||
}
|
||||
|
||||
// Encode a sized integer
|
||||
writeInt(length, value) {
|
||||
for (value &= 0xFFFFFFFF; length > 0; length--, value >>>= 8)
|
||||
this.data.push(value & 0xFF);
|
||||
}
|
||||
|
||||
// Encode a string as UTF-8 with prepended length
|
||||
writeString(value) {
|
||||
this.writeBytes(new TextEncoder().encode(value));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Generate the CRC32 lookup table
|
||||
let crcLookup = new Uint32Array(256);
|
||||
for (let x = 0; x <= 255; x++) {
|
||||
let l = x;
|
||||
for (let j = 7; j >= 0; j--)
|
||||
l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1)));
|
||||
crcLookup[x] = l;
|
||||
}
|
||||
|
||||
// Calculate the CRC32 checksum for a byte array
|
||||
function crc32(data) {
|
||||
let c = 0xFFFFFFFF;
|
||||
for (let x = 0; x < data.length; x++)
|
||||
c = ((c >>> 8) ^ crcLookup[(c ^ data[x]) & 0xFF]);
|
||||
return ~c & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
// Produce a .ZIP header from a bundled file
|
||||
function toZipHeader(file, crc32, offset) {
|
||||
let central = offset || offset === 0;
|
||||
let ret = new Bin();
|
||||
if (central) {
|
||||
ret.writeInt (4, 0x02014B50); // Signature
|
||||
ret.writeInt (2, 20); // Version created by
|
||||
} else
|
||||
ret.writeInt (4, 0x04034B50); // Signature
|
||||
ret.writeInt (2, 20); // Version required
|
||||
ret.writeInt (2, 0); // Bit flags
|
||||
ret.writeInt (2, 0); // Compression method
|
||||
ret.writeInt (2, 0); // Modified time
|
||||
ret.writeInt (2, 0); // Modified date
|
||||
ret.writeInt (4, crc32); // Checksum
|
||||
ret.writeInt (4, file.data.length); // Compressed size
|
||||
ret.writeInt (4, file.data.length); // Uncompressed size
|
||||
ret.writeInt (2, file.name.length); // Filename length
|
||||
ret.writeInt (2, 0); // Extra field length
|
||||
if (central) {
|
||||
ret.writeInt (2, 0); // File comment length
|
||||
ret.writeInt (2, 0); // Disk number start
|
||||
ret.writeInt (2, 0); // Internal attributes
|
||||
ret.writeInt (4, 0); // External attributes
|
||||
ret.writeInt (4, offset); // Relative offset
|
||||
}
|
||||
ret.writeString (file.name, true); // Filename
|
||||
if (!central)
|
||||
ret.writeBytes(file.data); // File data
|
||||
return ret.toByteArray();
|
||||
}
|
||||
|
||||
// Package all bundled files into a .zip file for download
|
||||
Bundle.save = function() {
|
||||
let centrals = new Array(manifest.length);
|
||||
let locals = new Array(manifest.length);
|
||||
let offset = 0;
|
||||
let size = 0;
|
||||
|
||||
// Encode file and directory entries
|
||||
let keys = Object.keys(Bundle.files);
|
||||
for (let x = 0; x < keys.length; x++) {
|
||||
let file = Bundle.get(keys[x]);
|
||||
let sum = crc32(file.data);
|
||||
locals [x] = toZipHeader(file, sum);
|
||||
centrals[x] = toZipHeader(file, sum, offset);
|
||||
offset += locals [x].length;
|
||||
size += centrals[x].length;
|
||||
}
|
||||
|
||||
// Encode end of central directory
|
||||
let end = new Bin();
|
||||
end.writeInt(4, 0x06054B50); // Signature
|
||||
end.writeInt(2, 0); // Disk number
|
||||
end.writeInt(2, 0); // Central dir start disk
|
||||
end.writeInt(2, centrals.length); // # central dir this disk
|
||||
end.writeInt(2, centrals.length); // # central dir total
|
||||
end.writeInt(4, size); // Size of central dir
|
||||
end.writeInt(4, offset); // Offset of central dir
|
||||
end.writeInt(2, 0); // .ZIP comment length
|
||||
|
||||
// Prompt the user to save the resulting file
|
||||
let a = document.createElement("a");
|
||||
a.download = bundleName + ".zip";
|
||||
a.href = URL.createObjectURL(new Blob(
|
||||
locals.concat(centrals).concat([end.toByteArray()]),
|
||||
{ type: "application/zip" }
|
||||
));
|
||||
a.click();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Boot Loader //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/*
|
||||
The Bundle.java utility prepends a manifest object to _boot.js that is an
|
||||
array of file infos, each of which has a name and size property.
|
||||
*/
|
||||
// De-register the boot function
|
||||
delete globalThis.a;
|
||||
|
||||
// Remove the bundle image element from the document
|
||||
Bundle.src = arguments[0].src;
|
||||
arguments[0].remove();
|
||||
|
||||
// Process all files from the bundle blob
|
||||
let blob = arguments[1];
|
||||
let offset = arguments[2] - manifest[0].size * 4;
|
||||
for (let file of manifest) {
|
||||
let data = new Uint8Array(file.size);
|
||||
for (let x = 0; x < file.size; x++, offset += 4)
|
||||
data[x] = blob[offset];
|
||||
if (file == manifest[0])
|
||||
offset += 4;
|
||||
Bundle.add(file.name, data);
|
||||
// Convert the file manifest into BundledFile objects
|
||||
let buffer = arguments[1];
|
||||
let offset = arguments[2] - manifest[0][1];
|
||||
for (let entry of manifest) {
|
||||
Bundle.add(entry[0], buffer.subarray(offset, offset + entry[1]));
|
||||
offset += entry[1];
|
||||
if (Bundle.length == 1)
|
||||
offset++; // Skip null delimiter
|
||||
}
|
||||
|
||||
// Program startup
|
||||
let run = async function() {
|
||||
|
||||
// Fonts
|
||||
for (let file of Object.values(Bundle.files)) {
|
||||
if (!file.name.endsWith(".woff2"))
|
||||
continue;
|
||||
let family = "/" + file.name;
|
||||
family = family.substring(family.lastIndexOf("/")+1, family.length-6);
|
||||
let font = new FontFace(family, file.data);
|
||||
await font.load();
|
||||
document.fonts.add(font);
|
||||
}
|
||||
|
||||
// Scripts
|
||||
await Bundle.run("app/App.js");
|
||||
await Bundle.run("app/Debugger.js");
|
||||
await Bundle.run("app/toolkit/Toolkit.js");
|
||||
await Bundle.run("app/toolkit/Component.js");
|
||||
await Bundle.run("app/toolkit/Panel.js");
|
||||
await Bundle.run("app/toolkit/Application.js");
|
||||
await Bundle.run("app/toolkit/Button.js");
|
||||
await Bundle.run("app/toolkit/ButtonGroup.js");
|
||||
await Bundle.run("app/toolkit/CheckBox.js");
|
||||
await Bundle.run("app/toolkit/Label.js");
|
||||
await Bundle.run("app/toolkit/MenuBar.js");
|
||||
await Bundle.run("app/toolkit/MenuItem.js");
|
||||
await Bundle.run("app/toolkit/Menu.js");
|
||||
await Bundle.run("app/toolkit/RadioButton.js");
|
||||
await Bundle.run("app/toolkit/Splitter.js");
|
||||
await Bundle.run("app/toolkit/TextBox.js");
|
||||
await Bundle.run("app/toolkit/Window.js");
|
||||
await Bundle.run("app/windows/CPUWindow.js");
|
||||
await Bundle.run("app/windows/Disassembler.js");
|
||||
await Bundle.run("app/windows/Register.js");
|
||||
await Bundle.run("app/windows/MemoryWindow.js");
|
||||
await App.create();
|
||||
};
|
||||
run();
|
||||
// Begin program operations
|
||||
import(Bundle.debug ? "./app/main.js" : Bundle["app/main.js"].toScript());
|
||||
|
|
|
@ -0,0 +1,444 @@
|
|||
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 };
|
|
@ -0,0 +1,106 @@
|
|||
import { Disassembler } from /**/"./Disassembler.js";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// CPU //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// CPU register editor and disassembler
|
||||
class CPU extends Toolkit.SplitPane {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, sim) {
|
||||
super(app, {
|
||||
className: "tk tk-splitpane tk-cpu",
|
||||
edge : Toolkit.SplitPane.RIGHT,
|
||||
style : {
|
||||
position: "relative"
|
||||
}
|
||||
});
|
||||
|
||||
this.app = app;
|
||||
this.sim = sim;
|
||||
this.initDasm();
|
||||
|
||||
this.metrics = new Toolkit.Component(this.app, {
|
||||
className: "tk tk-mono",
|
||||
tagName : "div",
|
||||
style : {
|
||||
position : "absolute",
|
||||
visibility: "hidden"
|
||||
}
|
||||
});
|
||||
let text = "";
|
||||
for (let x = 0; x < 16; x++) {
|
||||
if (x) text += "\n";
|
||||
let digit = x.toString(16);
|
||||
text += digit + "\n" + digit.toUpperCase();
|
||||
}
|
||||
this.metrics.element.innerText = text;
|
||||
this.splitter.append(this.metrics.element);
|
||||
|
||||
this.setView(1, this.regs = new Toolkit.SplitPane(app, {
|
||||
className: "tk tk-splitpane",
|
||||
edge : Toolkit.SplitPane.TOP
|
||||
}));
|
||||
|
||||
this.regs.setView(0, this.sysregs = new RegisterList(this, true ));
|
||||
this.regs.setView(1, this.proregs = new RegisterList(this, false));
|
||||
|
||||
// Adjust split panes to the initial size of the System Registers pane
|
||||
let resize;
|
||||
let preshow = e=>this.onPreshow(resize);
|
||||
resize = new ResizeObserver(preshow);
|
||||
resize.observe(this.sysregs.viewport);
|
||||
resize.observe(this.metrics.element);
|
||||
|
||||
this.metrics.addEventListener("resize", e=>this.metricsResize());
|
||||
}
|
||||
|
||||
initDasm() {
|
||||
this.dasm = new Disassembler(this.app, this.sim);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
|
||||
|
||||
// Resize handler prior to first visibility
|
||||
onPreshow(resize) {
|
||||
this.metricsResize();
|
||||
|
||||
// Once the list of registers is visible, stop listening
|
||||
if (this.isVisible()) {
|
||||
resize.disconnect();
|
||||
this.sysregs.view.element.style.display = "grid";
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the split panes
|
||||
let sys = this.sysregs.view.element;
|
||||
let pro = this.proregs.view.element;
|
||||
this.setValue(
|
||||
Math.max(sys.scrollWidth, pro.scrollWidth) +
|
||||
this.sysregs.vertical.getBounds().width
|
||||
);
|
||||
this.regs.setValue(
|
||||
this.sysregs[PSW].expansion.getBounds().bottom -
|
||||
sys.getBoundingClientRect().top
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export { CPU };
|
|
@ -0,0 +1,187 @@
|
|||
import { Disassembler } from /**/"./Disassembler.js";
|
||||
import { Memory } from /**/"./Memory.js";
|
||||
import { RegisterList } from /**/"./RegisterList.js";
|
||||
|
||||
|
||||
|
||||
class Debugger {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, index, sim) {
|
||||
|
||||
// Configure instance fields
|
||||
this.app = app;
|
||||
this.index = index;
|
||||
this.sim = sim;
|
||||
|
||||
// Configure components
|
||||
this.disassembler = new Disassembler(this);
|
||||
this.memory = new Memory (this);
|
||||
this.programRegisters = new RegisterList(this, false);
|
||||
this.systemRegisters = new RegisterList(this, true );
|
||||
|
||||
// Configure windows
|
||||
this.initCPUWindow ();
|
||||
this.initMemoryWindow();
|
||||
}
|
||||
|
||||
// Set up the CPU window
|
||||
initCPUWindow() {
|
||||
let app = this.app;
|
||||
|
||||
// Produce the window
|
||||
let wnd = this.cpuWindow = new app.Toolkit.Window(app, {
|
||||
width : 400,
|
||||
height : 300,
|
||||
className : "tk tk-window tk-cpu" + (this.index==0?"":" two"),
|
||||
closeToolTip: "common.close",
|
||||
title : "cpu._",
|
||||
visible : false
|
||||
});
|
||||
wnd.setSubstitution("sim", this.index == 1 ? " 2" : "");
|
||||
wnd.addEventListener("close", ()=>wnd.setVisible(false));
|
||||
app.desktop.add(wnd);
|
||||
|
||||
// Visibility override
|
||||
let that = this;
|
||||
let setVisible = wnd.setVisible;
|
||||
wnd.setVisible = function(visible) {
|
||||
that.disassembler .setSubscribed(visible);
|
||||
that.systemRegisters .setSubscribed(visible);
|
||||
that.programRegisters.setSubscribed(visible);
|
||||
setVisible.apply(wnd, arguments);
|
||||
};
|
||||
|
||||
// Auto-seek the initial view
|
||||
let onSeek = ()=>this.disassembler.seek(this.disassembler.pc, true);
|
||||
Toolkit.addResizeListener(this.disassembler.element, onSeek);
|
||||
wnd.addEventListener("firstshow", ()=>{
|
||||
app.desktop.center(wnd);
|
||||
Toolkit.removeResizeListener(this.disassembler.element, onSeek);
|
||||
});
|
||||
|
||||
// Window splitters
|
||||
let sptMain = new Toolkit.SplitPane(this.app, {
|
||||
className: "tk tk-splitpane tk-main",
|
||||
edge : Toolkit.SplitPane.RIGHT
|
||||
});
|
||||
let sptRegs = new Toolkit.SplitPane(this.app, {
|
||||
className: "tk tk-splitpane tk-registers",
|
||||
edge : Toolkit.SplitPane.TOP
|
||||
});
|
||||
|
||||
// Configure window splitter initial size
|
||||
let resize = new ResizeObserver(()=>{
|
||||
|
||||
// Measure register lists
|
||||
let sys = this.systemRegisters .getPreferredSize();
|
||||
let pro = this.programRegisters.getPreferredSize();
|
||||
let height = Math.ceil(Math.max(sys.height, pro.height));
|
||||
let width = Math.ceil(Math.max(sys.width , pro.width )) +
|
||||
this.systemRegisters.vertical.getBounds().width;
|
||||
|
||||
// Configure splitters
|
||||
if (sptMain.getValue() != width)
|
||||
sptMain.setValue(width);
|
||||
if (sptRegs.getValue() != height)
|
||||
sptRegs.setValue(height);
|
||||
});
|
||||
resize.observe(this.programRegisters.view.element);
|
||||
resize.observe(this.systemRegisters .view.element);
|
||||
|
||||
// Stop monitoring splitter size when something receives focus
|
||||
let onFocus = e=>{
|
||||
resize.disconnect();
|
||||
wnd.removeEventListener("focus", onFocus, true);
|
||||
};
|
||||
sptMain.addEventListener("focus", onFocus, true);
|
||||
|
||||
// Configure window layout
|
||||
sptMain.setView(0, this.disassembler);
|
||||
sptMain.setView(1, sptRegs);
|
||||
sptRegs.setView(0, this.systemRegisters);
|
||||
sptRegs.setView(1, this.programRegisters);
|
||||
wnd.append(sptMain);
|
||||
}
|
||||
|
||||
// Set up the Memory window
|
||||
initMemoryWindow() {
|
||||
let app = this.app;
|
||||
|
||||
// Produce the window
|
||||
let wnd = this.memoryWindow = new app.Toolkit.Window(app, {
|
||||
width : 400,
|
||||
height : 300,
|
||||
className : "tk tk-window" + (this.index == 0 ? "" : " two"),
|
||||
closeToolTip: "common.close",
|
||||
title : "memory._",
|
||||
visible : false
|
||||
});
|
||||
wnd.setSubstitution("sim", this.index == 1 ? " 2" : "");
|
||||
wnd.addEventListener("close" , ()=>wnd.setVisible(false));
|
||||
wnd.addEventListener("firstshow", ()=>app.desktop.center(wnd));
|
||||
app.desktop.add(wnd);
|
||||
|
||||
// Visibility override
|
||||
let that = this;
|
||||
let setVisible = wnd.setVisible;
|
||||
wnd.setVisible = function(visible) {
|
||||
that.memory.setSubscribed(visible);
|
||||
setVisible.apply(wnd, arguments);
|
||||
};
|
||||
|
||||
// Configure window layout
|
||||
wnd.append(this.memory);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Retrieve the disassembler configuraiton
|
||||
getDasmConfig() {
|
||||
return this.disassembler.getConfig();
|
||||
}
|
||||
|
||||
// Attempt to run until the next instruction
|
||||
runNext() {
|
||||
this.app.runNext(this.index);
|
||||
}
|
||||
|
||||
// Update the disassembler configuration
|
||||
setDasmConfig(config) {
|
||||
this.disassembler .setConfig(config);
|
||||
this.memory .dasmChanged();
|
||||
this.programRegisters.dasmChanged();
|
||||
this.systemRegisters .dasmChanged();
|
||||
}
|
||||
|
||||
// Specify whether dual sims mode is active
|
||||
setDualSims(dualSims) {
|
||||
|
||||
// Update substitutions for sim 1
|
||||
if (this.index == 0) {
|
||||
let sub = dualSims ? " 1" : "";
|
||||
this.cpuWindow .setSubstitution("sim", sub);
|
||||
this.memoryWindow.setSubstitution("sim", sub);
|
||||
}
|
||||
|
||||
// Hide windows for sim 2
|
||||
else if (!dualSims) {
|
||||
this.cpuWindow .close(false);
|
||||
this.memoryWindow.close(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Execute one instruction
|
||||
singleStep() {
|
||||
this.app.singleStep(this.index);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export { Debugger };
|
|
@ -0,0 +1,892 @@
|
|||
import { Util } from /**/"../app/Util.js";
|
||||
|
||||
|
||||
|
||||
// Opcode definition
|
||||
class Opdef {
|
||||
constructor(format, mnemonic, signExtend) {
|
||||
this.format = format;
|
||||
this.mnemonic = mnemonic;
|
||||
this.signExtend = !!signExtend;
|
||||
}
|
||||
}
|
||||
|
||||
// Top-level opcode definition lookup table by opcode
|
||||
let OPDEFS = [
|
||||
new Opdef(1, "MOV" ), new Opdef(1, "ADD" ), new Opdef(1, "SUB" ),
|
||||
new Opdef(1, "CMP" ), new Opdef(1, "SHL" ), new Opdef(1, "SHR" ),
|
||||
new Opdef(1, "JMP" ), new Opdef(1, "SAR" ), new Opdef(1, "MUL" ),
|
||||
new Opdef(1, "DIV" ), new Opdef(1, "MULU" ), new Opdef(1, "DIVU" ),
|
||||
new Opdef(1, "OR" ), new Opdef(1, "AND" ), new Opdef(1, "XOR" ),
|
||||
new Opdef(1, "NOT" ), new Opdef(2, "MOV" ,1), new Opdef(2, "ADD",1),
|
||||
new Opdef(2, "SETF" ), new Opdef(2, "CMP" ,1), new Opdef(2, "SHL" ),
|
||||
new Opdef(2, "SHR" ), new Opdef(2, "CLI" ), new Opdef(2, "SAR" ),
|
||||
new Opdef(2, "TRAP" ), new Opdef(2, "RETI" ), new Opdef(2, "HALT" ),
|
||||
new Opdef(0, null ), new Opdef(2, "LDSR" ), new Opdef(2, "STSR" ),
|
||||
new Opdef(2, "SEI" ), new Opdef(2, null ), new Opdef(3, "Bcond"),
|
||||
new Opdef(3, "Bcond"), new Opdef(3, "Bcond" ), new Opdef(3, "Bcond"),
|
||||
new Opdef(3, "Bcond"), new Opdef(3, "Bcond" ), new Opdef(3, "Bcond"),
|
||||
new Opdef(3, "Bcond"), new Opdef(5,"MOVEA",1), new Opdef(5,"ADDI",1),
|
||||
new Opdef(4, "JR" ), new Opdef(4, "JAL" ), new Opdef(5, "ORI" ),
|
||||
new Opdef(5, "ANDI" ), new Opdef(5, "XORI" ), new Opdef(5, "MOVHI"),
|
||||
new Opdef(6, "LD.B" ), new Opdef(6, "LD.H" ), new Opdef(0, null ),
|
||||
new Opdef(6, "LD.W" ), new Opdef(6, "ST.B" ), new Opdef(6, "ST.H" ),
|
||||
new Opdef(0, null ), new Opdef(6, "ST.W" ), new Opdef(6, "IN.B" ),
|
||||
new Opdef(6, "IN.H" ), new Opdef(6, "CAXI" ), new Opdef(6, "IN.W" ),
|
||||
new Opdef(6, "OUT.B"), new Opdef(6, "OUT.H" ), new Opdef(7, null ),
|
||||
new Opdef(6, "OUT.W")
|
||||
];
|
||||
|
||||
// Bit string mnemonic lookup table by sub-opcode
|
||||
let BITSTRINGS = [
|
||||
"SCH0BSU", "SCH0BSD", "SCH1BSU", "SCH1BSD",
|
||||
null , null , null , null ,
|
||||
"ORBSU" , "ANDBSU" , "XORBSU" , "MOVBSU" ,
|
||||
"ORNBSU" , "ANDNBSU", "XORNBSU", "NOTBSU"
|
||||
];
|
||||
|
||||
// Floating-point/Nintendo mnemonic lookup table by sub-opcode
|
||||
let FLOATENDOS = [
|
||||
"CMPF.S", null , "CVT.WS", "CVT.SW" ,
|
||||
"ADDF.S", "SUBF.S", "MULF.S", "DIVF.S" ,
|
||||
"XB" , "XH" , "REV" , "TRNC.SW",
|
||||
"MPYHW"
|
||||
];
|
||||
|
||||
// Program register names
|
||||
let PROREGS = { 2: "hp", 3: "sp", 4: "gp", 5: "tp", 31: "lp" };
|
||||
|
||||
// System register names
|
||||
let SYSREGS = [
|
||||
"EIPC", "EIPSW", "FEPC", "FEPSW",
|
||||
"ECR" , "PSW" , "PIR" , "TKCW" ,
|
||||
null , null , null , null ,
|
||||
null , null , null , null ,
|
||||
null , null , null , null ,
|
||||
null , null , null , null ,
|
||||
"CHCW", "ADTRE", null , null ,
|
||||
null , null , null , null
|
||||
];
|
||||
|
||||
// Condition mnemonics
|
||||
let CONDS = [
|
||||
"V" , ["C" , "L" ], ["E" , "Z" ], "NH",
|
||||
"N" , "T" , "LT" , "LE",
|
||||
"NV", ["NC", "NL"], ["NE", "NZ"], "H" ,
|
||||
"P" , "F" , "GE" , "GT"
|
||||
];
|
||||
|
||||
// Output setting keys
|
||||
const SETTINGS = [
|
||||
"bcondMerged", "branchAddress", "condCase", "condCL", "condEZ",
|
||||
"condNames", "hexCaps", "hexDollar", "hexSuffix", "imm5OtherHex",
|
||||
"imm5ShiftHex", "imm5TrapHex", "imm16AddiLargeHex", "imm16AddiSmallHex",
|
||||
"imm16MoveHex", "imm16OtherHex", "jmpBrackets", "memoryLargeHex",
|
||||
"memorySmallHex", "memoryInside", "mnemonicCaps", "operandReverse",
|
||||
"proregCaps", "proregNames", "setfMerged", "sysregCaps", "sysregNames"
|
||||
];
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Line //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// One line of output
|
||||
class Line {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(parent, first) {
|
||||
|
||||
// Configure instance fields
|
||||
this.parent = parent;
|
||||
|
||||
// Configure labels
|
||||
this.lblAddress = this.label("tk-address" , first);
|
||||
this.lblBytes = this.label("tk-bytes" , first);
|
||||
this.lblMnemonic = this.label("tk-mnemonic", first);
|
||||
this.lblOperands = this.label("tk-operands", false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update the elements' display
|
||||
refresh(row, isPC) {
|
||||
|
||||
// The row is not available
|
||||
if (!row) {
|
||||
this.lblAddress .innerText = "--------";
|
||||
this.lblBytes .innerText = "";
|
||||
this.lblMnemonic.innerText = "---";
|
||||
this.lblOperands.innerText = "";
|
||||
}
|
||||
|
||||
// Update labels with the disassembled row's contents
|
||||
else {
|
||||
this.lblAddress .innerText = row.address;
|
||||
this.lblBytes .innerText = row.bytes;
|
||||
this.lblMnemonic.innerText = row.mnemonic;
|
||||
this.lblOperands.innerText = row.operands;
|
||||
}
|
||||
|
||||
// Update style according to selection
|
||||
let method = isPC ? "add" : "remove";
|
||||
this.lblAddress .classList[method]("tk-selected");
|
||||
this.lblBytes .classList[method]("tk-selected");
|
||||
this.lblMnemonic.classList[method]("tk-selected");
|
||||
this.lblOperands.classList[method]("tk-selected");
|
||||
}
|
||||
|
||||
// Specify whether the elements on this line are visible
|
||||
setVisible(visible) {
|
||||
visible = visible ? "block" : "none";
|
||||
this.lblAddress .style.display = visible;
|
||||
this.lblBytes .style.display = visible;
|
||||
this.lblMnemonic.style.display = visible;
|
||||
this.lblOperands.style.display = visible;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Create a display label
|
||||
label(className, first) {
|
||||
|
||||
// Create the label element
|
||||
let label = document.createElement("div");
|
||||
label.className = "tk " + className;
|
||||
|
||||
// The label is part of the first row of output
|
||||
let element = label;
|
||||
if (first) {
|
||||
|
||||
// Create a container element
|
||||
element = document.createElement("div");
|
||||
element.append(label);
|
||||
element.max = 0;
|
||||
|
||||
// Ensure the container can always fit the column contents
|
||||
Toolkit.addResizeListener(element, ()=>{
|
||||
let width = Math.ceil(label.getBoundingClientRect().width);
|
||||
if (width <= element.max)
|
||||
return;
|
||||
element.max = width;
|
||||
element.style.minWidth = width + "px";
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Configure elements
|
||||
this.parent.view.append(element);
|
||||
return label;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Disassembler //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Text disassembler for NVC
|
||||
class Disassembler extends Toolkit.ScrollPane {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(debug) {
|
||||
super(debug.app, {
|
||||
className : "tk tk-scrollpane tk-disassembler",
|
||||
horizontal: Toolkit.ScrollPane.AS_NEEDED,
|
||||
focusable : true,
|
||||
tabStop : true,
|
||||
tagName : "div",
|
||||
vertical : Toolkit.ScrollPane.NEVER
|
||||
});
|
||||
|
||||
// Configure instance fields
|
||||
this.address = Util.u32(0xFFFFFFF0);
|
||||
this.app = debug.app;
|
||||
this.columns = [ 0, 0, 0, 0 ];
|
||||
this.data = [];
|
||||
this.debug = debug;
|
||||
this.isSubscribed = false;
|
||||
this.lines = null;
|
||||
this.pc = this.address;
|
||||
this.pending = [];
|
||||
this.rows = [];
|
||||
this.scroll = 0;
|
||||
this.sim = debug.sim;
|
||||
|
||||
// Default output settings
|
||||
this.setConfig({
|
||||
bcondMerged : true,
|
||||
branchAddress : true,
|
||||
condCase : false,
|
||||
condCL : 1,
|
||||
condEZ : 1,
|
||||
condNames : true,
|
||||
hexCaps : true,
|
||||
hexDollar : false,
|
||||
hexSuffix : false,
|
||||
imm5OtherHex : false,
|
||||
imm5ShiftHex : false,
|
||||
imm5TrapHex : false,
|
||||
imm16AddiLargeHex: true,
|
||||
imm16AddiSmallHex: false,
|
||||
imm16MoveHex : true,
|
||||
imm16OtherHex : true,
|
||||
jmpBrackets : true,
|
||||
memoryLargeHex : true,
|
||||
memorySmallHex : false,
|
||||
memoryInside : false,
|
||||
mnemonicCaps : true,
|
||||
operandReverse : false,
|
||||
proregCaps : false,
|
||||
proregNames : true,
|
||||
setfMerged : false,
|
||||
sysregCaps : true,
|
||||
sysregNames : true
|
||||
});
|
||||
|
||||
// Configure viewport
|
||||
this.viewport.classList.add("tk-mono");
|
||||
|
||||
// Configure view
|
||||
let view = document.createElement("div");
|
||||
view.className = "tk tk-view";
|
||||
Object.assign(view.style, {
|
||||
display : "grid",
|
||||
gridTemplateColumns: "repeat(3, max-content) auto"
|
||||
});
|
||||
this.setView(view);
|
||||
|
||||
// Font-measuring element
|
||||
this.metrics = new Toolkit.Component(this.app, {
|
||||
className: "tk tk-metrics tk-mono",
|
||||
tagName : "div",
|
||||
style : {
|
||||
position : "absolute",
|
||||
visibility: "hidden"
|
||||
}
|
||||
});
|
||||
this.metrics.element.innerText = "X";
|
||||
this.append(this.metrics.element);
|
||||
|
||||
// First row always exists
|
||||
this.lines = [ new Line(this, true) ];
|
||||
|
||||
// Configure event handlers
|
||||
Toolkit.addResizeListener(this.viewport, e=>this.onResize(e));
|
||||
this.addEventListener("keydown", e=>this.onKeyDown (e));
|
||||
this.addEventListener("wheel" , e=>this.onMouseWheel(e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
let tall = this.tall(false);
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
|
||||
// Navigation
|
||||
case "ArrowDown" : this.fetch(+1 , true); break;
|
||||
case "ArrowUp" : this.fetch(-1 , true); break;
|
||||
case "PageDown" : this.fetch(+tall, true); break;
|
||||
case "PageUp" : this.fetch(-tall, true); break;
|
||||
|
||||
// View control
|
||||
case "ArrowLeft" : this.horizontal.setValue(
|
||||
this.horizontal.value - this.horizontal.increment); break;
|
||||
case "ArrowRight": this.horizontal.setValue(
|
||||
this.horizontal.value + this.horizontal.increment); break;
|
||||
|
||||
// Goto
|
||||
case "g": case "G":
|
||||
if (!e.ctrlKey)
|
||||
return;
|
||||
this.promptGoto();
|
||||
break;
|
||||
|
||||
// Single step
|
||||
case "F10":
|
||||
this.debug.runNext();
|
||||
break;
|
||||
|
||||
// Single step
|
||||
case "F11":
|
||||
this.debug.singleStep();
|
||||
break;
|
||||
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Mouse wheel
|
||||
onMouseWheel(e) {
|
||||
|
||||
// User agent scaling action
|
||||
if (e.ctrlKey)
|
||||
return;
|
||||
|
||||
// No rotation has occurred
|
||||
let offset = Math.sign(e.deltaY) * 3;
|
||||
if (offset == 0)
|
||||
return;
|
||||
|
||||
// Update the display address
|
||||
this.fetch(offset, true);
|
||||
}
|
||||
|
||||
// Viewport resized
|
||||
onResize(e) {
|
||||
let fetch = false;
|
||||
let tall = this.tall(true);
|
||||
|
||||
// Add additional lines to the output
|
||||
for (let x = 0; x < tall; x++) {
|
||||
if (x >= this.lines.length) {
|
||||
fetch = true;
|
||||
this.lines.push(new Line(this));
|
||||
}
|
||||
this.lines[x].setVisible(true);
|
||||
}
|
||||
|
||||
// Remove extra lines from the output
|
||||
for (let x = tall; x < this.lines.length; x++)
|
||||
this.lines[x].setVisible(false);
|
||||
|
||||
// Configure horizontal scroll bar
|
||||
if (this.metrics)
|
||||
this.horizontal.setIncrement(this.metrics.getBounds().width);
|
||||
|
||||
// Update the display
|
||||
if (fetch)
|
||||
this.fetch(0, true);
|
||||
else this.refresh();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Produce disassembly text
|
||||
disassemble(rows) {
|
||||
|
||||
// Produce a deep copy of the input list
|
||||
let copy = new Array(rows.length);
|
||||
for (let x = 0; x < rows.length; x++) {
|
||||
copy[x] = {};
|
||||
Object.assign(copy[x], rows[x]);
|
||||
}
|
||||
rows = copy;
|
||||
|
||||
// Process all rows
|
||||
for (let row of rows) {
|
||||
row.operands = [];
|
||||
|
||||
// Read instruction bits from the bus
|
||||
let bits0 = row.bytes[1] << 8 | row.bytes[0];
|
||||
let bits1;
|
||||
if (row.bytes.length == 4)
|
||||
bits1 = row.bytes[3] << 8 | row.bytes[2];
|
||||
|
||||
// Working variables
|
||||
let opcode = bits0 >> 10;
|
||||
let opdef = OPDEFS[opcode];
|
||||
|
||||
// Sub-opcode mnemonics
|
||||
if (row.opcode == 0b011111)
|
||||
row.mnemonic = BITSTRINGS[bits0 & 31] || "---";
|
||||
else if (row.opcode == 0b111110)
|
||||
row.mnemonic = FLOATENDOS[bits1 >> 10 & 63] || "---";
|
||||
else row.mnemonic = opdef.mnemonic;
|
||||
|
||||
// Processing by format
|
||||
switch (opdef.format) {
|
||||
case 1: this.formatI (row, bits0 ); break;
|
||||
case 3: this.formatIII(row, bits0 ); break;
|
||||
case 4: this.formatIV (row, bits0, bits1); break;
|
||||
case 6: this.formatVI (row, bits0, bits1); break;
|
||||
case 7: this.formatVII(row, bits0 ); break;
|
||||
case 2:
|
||||
this.formatII(row, bits0, opdef.signExtend); break;
|
||||
case 5:
|
||||
this.formatV (row, bits0, bits1, opdef.signExtend);
|
||||
}
|
||||
|
||||
// Format bytes
|
||||
let text = [];
|
||||
for (let x = 0; x < row.bytes.length; x++)
|
||||
text.push(row.bytes[x].toString(16).padStart(2, "0"));
|
||||
row.bytes = text.join(" ");
|
||||
|
||||
// Post-processing
|
||||
row.address = row.address.toString(16).padStart(8, "0");
|
||||
if (this.hexCaps) {
|
||||
row.address = row.address.toUpperCase();
|
||||
row.bytes = row.bytes .toUpperCase();
|
||||
}
|
||||
if (!this.mnemonicCaps)
|
||||
row.mnemonic = row.mnemonic.toLowerCase();
|
||||
if (this.operandReverse)
|
||||
row.operands.reverse();
|
||||
row.operands = row.operands.join(", ");
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Retrieve all output settings in an object
|
||||
getConfig() {
|
||||
let ret = {};
|
||||
for (let key of SETTINGS)
|
||||
ret[key] = this[key];
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Update with disassembly state from the core
|
||||
refresh(data = 0) {
|
||||
let bias;
|
||||
|
||||
// Scrolling prefresh
|
||||
if (typeof data == "number")
|
||||
bias = 16 + data;
|
||||
|
||||
// Received data from the core thread
|
||||
else {
|
||||
this.data = data.rows;
|
||||
this.pc = data.pc;
|
||||
if (this.data.length == 0)
|
||||
return;
|
||||
this.address = this.data[0].address;
|
||||
this.rows = this.disassemble(this.data);
|
||||
bias = 16 +
|
||||
(data.scroll === null ? 0 : this.scroll - data.scroll);
|
||||
}
|
||||
|
||||
// Update elements
|
||||
let count = Math.min(this.tall(true), this.data.length);
|
||||
for (let y = 0; y < count; y++) {
|
||||
let index = bias + y;
|
||||
let line = this.data[index];
|
||||
let row = this.rows[index];
|
||||
this.lines[y].refresh(row, line && line.address == this.pc);
|
||||
}
|
||||
|
||||
// Refesh scroll pane
|
||||
this.update();
|
||||
}
|
||||
|
||||
// Bring an address into view
|
||||
seek(address, force) {
|
||||
|
||||
// Check if the address is already in the view
|
||||
if (!force) {
|
||||
let bias = 16;
|
||||
let tall = this.tall(false);
|
||||
let count = Math.min(tall, this.data.length);
|
||||
|
||||
// The address is currently visible in the output
|
||||
for (let y = 0; y < count; y++) {
|
||||
let row = this.data[bias + y];
|
||||
if (!row || Util.u32(address - row.address) >= row.size)
|
||||
continue;
|
||||
|
||||
// The address is on this row
|
||||
this.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Place the address at a particular position in the view
|
||||
this.address = address;
|
||||
this.fetch(null);
|
||||
}
|
||||
|
||||
// Update output settings
|
||||
setConfig(config) {
|
||||
|
||||
// Update settings
|
||||
for (let key of SETTINGS)
|
||||
if (key in config)
|
||||
this[key] = config[key];
|
||||
|
||||
// Regenerate output
|
||||
this.refresh({
|
||||
pc : this.pc,
|
||||
rows : this.data,
|
||||
scroll: null
|
||||
});
|
||||
}
|
||||
|
||||
// Subscribe to or unsubscribe from core updates
|
||||
setSubscribed(subscribed) {
|
||||
subscribed = !!subscribed;
|
||||
|
||||
// Nothing to change
|
||||
if (subscribed == this.isSubscribed)
|
||||
return;
|
||||
|
||||
// Configure instance fields
|
||||
this.isSubscribed = subscribed;
|
||||
|
||||
// Subscribe to core updates
|
||||
if (subscribed)
|
||||
this.fetch(0);
|
||||
|
||||
// Unsubscribe from core updates
|
||||
else this.sim.unsubscribe("dasm");
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Select a condition's name
|
||||
cond(cond) {
|
||||
let ret = CONDS[cond];
|
||||
switch (cond) {
|
||||
case 1: case 9: return CONDS[cond][this.condCL];
|
||||
case 2: case 10: return CONDS[cond][this.condEZ];
|
||||
}
|
||||
return CONDS[cond];
|
||||
}
|
||||
|
||||
// Retrieve disassembly data from the core
|
||||
async fetch(scroll, prefresh) {
|
||||
let row;
|
||||
|
||||
// Scrolling relative to the current view
|
||||
if (scroll) {
|
||||
if (prefresh)
|
||||
this.refresh(scroll);
|
||||
this.scroll = Util.s32(this.scroll + scroll);
|
||||
row = -scroll;
|
||||
}
|
||||
|
||||
// Jumping to an address directly
|
||||
else row = scroll === null ? Math.floor(this.tall(false) / 3) + 16 : 0;
|
||||
|
||||
// Retrieve data from the core
|
||||
this.refresh(
|
||||
await this.sim.disassemble(
|
||||
this.address,
|
||||
row,
|
||||
this.tall(true) + 32,
|
||||
scroll === null ? null : this.scroll, {
|
||||
subscribe: this.isSubscribed && "dasm"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Represent a hexadecimal value
|
||||
hex(value, digits) {
|
||||
let sign = Util.s32(value) < 0 ? "-" : "";
|
||||
let ret = Math.abs(Util.u32(value)).toString(16).padStart(digits,"0");
|
||||
if (this.hexCaps)
|
||||
ret = ret.toUpperCase();
|
||||
if (this.hexSuffix)
|
||||
ret = ("abcdefABCDEF".indexOf(ret[0]) == -1 ? "" : "0") +
|
||||
ret + "h";
|
||||
else ret = (this.hexDollar ? "$" : "0x") + ret;
|
||||
return sign + ret;
|
||||
}
|
||||
|
||||
// Prompt the user to specify a new address
|
||||
promptGoto() {
|
||||
|
||||
// Receive input from the user
|
||||
let address = prompt(this.app.translate("common.gotoPrompt"));
|
||||
if (address == null)
|
||||
return;
|
||||
|
||||
// Process the input as an address in hexadecimal
|
||||
address = parseInt(address, 16);
|
||||
if (isNaN(address))
|
||||
return;
|
||||
|
||||
// Move the selection and refresh the display
|
||||
this.seek(Util.u32(address));
|
||||
}
|
||||
|
||||
// Select a program register name
|
||||
proreg(index) {
|
||||
let ret = this.proregNames && PROREGS[index] || "r" + index;
|
||||
return this.proregCaps ? ret.toUpperCase() : ret;
|
||||
}
|
||||
|
||||
// Measure how many rows of output are visible
|
||||
tall(partial) {
|
||||
let lineHeight = !this.metrics ? 0 :
|
||||
Math.ceil(this.metrics.getBounds().height);
|
||||
return lineHeight <= 0 ? 1 : Math.max(1, Math[partial?"ceil":"floor"](
|
||||
this.viewport.getBoundingClientRect().height / lineHeight));
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////// Decoding Methods /////////////////////////////
|
||||
|
||||
// Disassemble a Format I instruction
|
||||
formatI(row, bits0) {
|
||||
let reg1 = this.proreg(bits0 & 31);
|
||||
|
||||
// JMP
|
||||
if (row.mnemonic == "JMP") {
|
||||
if (this.jmpBrackets)
|
||||
reg1 = "[" + reg1 + "]";
|
||||
row.operands.push(reg1);
|
||||
}
|
||||
|
||||
// Other instructions
|
||||
else {
|
||||
let reg2 = this.proreg(bits0 >> 5 & 31);
|
||||
row.operands.push(reg1, reg2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Disassemble a Format II instruction
|
||||
formatII(row, bits0, signExtend) {
|
||||
|
||||
// Bit-string instructions are zero-operand
|
||||
if (bits0 >> 10 == 0b011111)
|
||||
return;
|
||||
|
||||
// Processing by mnemonic
|
||||
switch (row.mnemonic) {
|
||||
|
||||
// Zero-operand
|
||||
case "---" : // Fallthrough
|
||||
case "CLI" : // Fallthrough
|
||||
case "HALT": // Fallthrough
|
||||
case "RETI": // Fallthrough
|
||||
case "SEI" : return;
|
||||
|
||||
// Distinct notation
|
||||
case "LDSR": return this.ldstsr(row, bits0, true );
|
||||
case "SETF": return this.setf (row, bits0 );
|
||||
case "STSR": return this.ldstsr(row, bits0, false);
|
||||
}
|
||||
|
||||
// Retrieve immediate operand
|
||||
let imm = bits0 & 31;
|
||||
if (signExtend)
|
||||
imm = Util.signExtend(bits0, 5);
|
||||
|
||||
// TRAP instruction is one-operand
|
||||
if (row.mnemonic == "TRAP") {
|
||||
row.operands.push(this.trapHex ?
|
||||
this.hex(imm, 1) : imm.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
// Processing by mnemonic
|
||||
let hex = this.imm5OtherHex;
|
||||
switch (row.mnemonic) {
|
||||
case "SAR": // Fallthrough
|
||||
case "SHL": // Fallthrough
|
||||
case "SHR": hex = this.imm5ShiftHex;
|
||||
}
|
||||
imm = hex ? this.hex(imm, 1) : imm.toString();
|
||||
|
||||
// Two-operand instruction
|
||||
let reg2 = this.proreg(bits0 >> 5 & 31);
|
||||
row.operands.push(imm, reg2);
|
||||
}
|
||||
|
||||
// Disassemble a Format III instruction
|
||||
formatIII(row, bits0) {
|
||||
let cond = this.cond(bits0 >> 9 & 15);
|
||||
let disp = Util.signExtend(bits0 & 0x1FF, 9);
|
||||
|
||||
// Condition merged with mnemonic
|
||||
if (this.bcondMerged) {
|
||||
switch (cond) {
|
||||
case "F": row.mnemonic = "NOP"; return;
|
||||
case "T": row.mnemonic = "BR" ; break;
|
||||
default : row.mnemonic = "B" + cond;
|
||||
}
|
||||
}
|
||||
|
||||
// Condition as operand
|
||||
else {
|
||||
if (!this.condCaps)
|
||||
cond = cond.toLowerCase();
|
||||
row.operands.push(cond);
|
||||
}
|
||||
|
||||
// Operand as destination address
|
||||
if (this.branchAddress) {
|
||||
disp = Util.u32(row.address + disp & 0xFFFFFFFE)
|
||||
.toString(16).padStart(8, "0");
|
||||
if (this.hexCaps)
|
||||
disp = disp.toUpperCase();
|
||||
row.operands.push(disp);
|
||||
}
|
||||
|
||||
// Operand as displacement
|
||||
else {
|
||||
let sign = disp < 0 ? "-" : disp > 0 ? "+" : "";
|
||||
let rel = this.hex(Math.abs(disp), 1);
|
||||
row.operands.push(sign + rel);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Disassemble a Format IV instruction
|
||||
formatIV(row, bits0, bits1) {
|
||||
let disp = Util.signExtend(bits0 << 16 | bits1, 26);
|
||||
|
||||
// Operand as destination address
|
||||
if (this.branchAddress) {
|
||||
disp = Util.u32(row.address + disp & 0xFFFFFFFE)
|
||||
.toString(16).padStart(8, "0");
|
||||
if (this.hexCaps)
|
||||
disp = disp.toUpperCase();
|
||||
row.operands.push(disp);
|
||||
}
|
||||
|
||||
// Operand as displacement
|
||||
else {
|
||||
let sign = disp < 0 ? "-" : disp > 0 ? "+" : "";
|
||||
let rel = this.hex(Math.abs(disp), 1);
|
||||
row.operands.push(sign + rel);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Disassemble a Format V instruction
|
||||
formatV(row, bits0, bits1, signExtend) {
|
||||
let imm = signExtend ? Util.signExtend(bits1) : bits1;
|
||||
let reg1 = this.proreg(bits0 & 31);
|
||||
let reg2 = this.proreg(bits0 >> 5 & 31);
|
||||
|
||||
if (
|
||||
row.mnemonic == "ADDI" ?
|
||||
Math.abs(imm) <= 256 ?
|
||||
this.imm16AddiSmallHex :
|
||||
this.imm16AddiLargeHex
|
||||
: row.mnemonic == "MOVEA" || row.mnemonic == "MOVHI" ?
|
||||
this.imm16MoveHex
|
||||
:
|
||||
this.imm16OtherHex
|
||||
) imm = this.hex(imm, 4);
|
||||
|
||||
row.operands.push(imm, reg1, reg2);
|
||||
}
|
||||
|
||||
// Disassemble a Format VI instruction
|
||||
formatVI(row, bits0, bits1) {
|
||||
let disp = Util.signExtend(bits1);
|
||||
let reg1 = this.proreg(bits0 & 31);
|
||||
let reg2 = this.proreg(bits0 >> 5 & 31);
|
||||
let sign =
|
||||
disp < 0 ? "-" :
|
||||
disp == 0 || !this.memoryInside ? "" :
|
||||
"+";
|
||||
|
||||
// Displacement is hexadecimal
|
||||
disp = Math.abs(disp);
|
||||
if (disp == 0)
|
||||
disp = ""
|
||||
else if (disp <= 256 ? this.memorySmallHex : this.memoryLargeHex)
|
||||
disp = this.hex(disp, 1);
|
||||
|
||||
// Format the displacement figure according to its presentation
|
||||
disp = this.memoryInside ?
|
||||
sign == "" ? "" : " " + sign + " " + disp :
|
||||
sign + disp
|
||||
;
|
||||
|
||||
// Apply operands
|
||||
row.operands.push(this.memoryInside ?
|
||||
"[" + reg1 + disp + "]" :
|
||||
disp + "[" + reg1 + "]",
|
||||
reg2);
|
||||
|
||||
// Swap operands for output and store instructions
|
||||
switch (row.mnemonic) {
|
||||
case "OUT.B": case "OUT.H": case "OUT.W":
|
||||
case "ST.B" : case "ST.H" : case "ST.W" :
|
||||
row.operands.reverse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Disassemble a Format VII instruction
|
||||
formatVII(row, bits0) {
|
||||
let reg1 = this.proreg(bits0 & 31);
|
||||
let reg2 = this.proreg(bits0 >> 5 & 31);
|
||||
|
||||
// Invalid sub-opcode is zero-operand
|
||||
if (row.mnemonic == "---")
|
||||
return;
|
||||
|
||||
// Processing by mnemonic
|
||||
switch (row.mnemonic) {
|
||||
case "XB": // Fallthrough
|
||||
case "XH": break;
|
||||
default : row.operands.push(reg1);
|
||||
}
|
||||
|
||||
row.operands.push(reg2);
|
||||
}
|
||||
|
||||
// Format an LDSR or STSR instruction
|
||||
ldstsr(row, bits0, reverse) {
|
||||
|
||||
// System register
|
||||
let sysreg = bits0 & 31;
|
||||
sysreg = this.sysregNames && SYSREGS[sysreg] || sysreg.toString();
|
||||
if (!this.sysregCaps)
|
||||
sysreg = sysreg.toLowerCase();
|
||||
|
||||
// Program register
|
||||
let reg2 = this.proreg(bits0 >> 5 & 31);
|
||||
|
||||
// Operands
|
||||
row.operands.push(sysreg, reg2);
|
||||
if (reverse)
|
||||
row.operands.reverse();
|
||||
}
|
||||
|
||||
// Format a SETF instruction
|
||||
setf(row, bits0) {
|
||||
let cond = this.cond (bits0 & 15);
|
||||
let reg2 = this.proreg(bits0 >> 5 & 31);
|
||||
|
||||
// Condition merged with mnemonic
|
||||
if (!this.bcondMerged) {
|
||||
row.mnemonic += cond;
|
||||
}
|
||||
|
||||
// Condition as operand
|
||||
else {
|
||||
if (!this.condCaps)
|
||||
cond = cond.toLowerCase();
|
||||
row.operands.push(cond);
|
||||
}
|
||||
|
||||
row.operands.push(reg2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export { Disassembler };
|
|
@ -0,0 +1,467 @@
|
|||
import { Util } from /**/"./Util.js";
|
||||
|
||||
|
||||
|
||||
// Text to hex digit conversion
|
||||
const DIGITS = {
|
||||
"0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7,
|
||||
"8": 8, "9": 9, "A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15
|
||||
};
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Line //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// One line of output
|
||||
class Line {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(parent, index) {
|
||||
|
||||
// Configure instance fields
|
||||
this.index = index;
|
||||
this.parent = parent;
|
||||
|
||||
// Address label
|
||||
this.lblAddress = document.createElement("div");
|
||||
this.lblAddress.className = "tk tk-address";
|
||||
parent.view.appendChild(this.lblAddress);
|
||||
|
||||
// Byte labels
|
||||
this.lblBytes = new Array(16);
|
||||
for (let x = 0; x < 16; x++) {
|
||||
let lbl = this.lblBytes[x] = document.createElement("div");
|
||||
lbl.className = "tk tk-byte tk-" + x.toString(16);
|
||||
parent.view.appendChild(lbl);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update the elements' display
|
||||
refresh() {
|
||||
let address = Util.u32(this.parent.address + this.index * 16);
|
||||
let data = this.parent.data;
|
||||
let dataAddress = this.parent.dataAddress;
|
||||
let hexCaps = this.parent.dasm.hexCaps;
|
||||
let offset = Util.s32(address - dataAddress);
|
||||
|
||||
// Format the line's address
|
||||
let text = address.toString(16).padStart(8, "0");
|
||||
if (hexCaps)
|
||||
text = text.toUpperCase();
|
||||
this.lblAddress.innerText = text;
|
||||
|
||||
// The line's data is not available
|
||||
if (offset < 0 || offset >= data.length)
|
||||
for (let lbl of this.lblBytes)
|
||||
lbl.innerText = "--";
|
||||
|
||||
// The line's data is available
|
||||
else for (let x = 0; x < 16; x++, offset++) {
|
||||
let lbl = this.lblBytes[x];
|
||||
text = data[offset].toString(16).padStart(2, "0");
|
||||
|
||||
// The byte is the current selection
|
||||
if (Util.u32(address + x) == this.parent.selection) {
|
||||
lbl.classList.add("selected");
|
||||
if (this.parent.digit !== null)
|
||||
text = this.parent.digit.toString(16);
|
||||
}
|
||||
|
||||
// The byte is not the current selection
|
||||
else lbl.classList.remove("selected");
|
||||
|
||||
// Update the label's text
|
||||
if (hexCaps)
|
||||
text = text.toUpperCase();
|
||||
lbl.innerText = text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Specify whether the elements on this line are visible
|
||||
setVisible(visible) {
|
||||
visible = visible ? "block" : "none";
|
||||
this.lblAddress.style.display = visible;
|
||||
for (let lbl of this.lblBytes)
|
||||
lbl.style.display = visible;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Memory //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Memory hex editor
|
||||
class Memory extends Toolkit.ScrollPane {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(debug) {
|
||||
super(debug.app, {
|
||||
className : "tk tk-scrollpane tk-memory",
|
||||
horizontal: Toolkit.ScrollPane.AS_NEEDED,
|
||||
focusable : true,
|
||||
tabStop : true,
|
||||
tagName : "div",
|
||||
vertical : Toolkit.ScrollPane.NEVER
|
||||
});
|
||||
|
||||
// Configure instance fields
|
||||
this.address = 0x05000000;
|
||||
this.app = debug.app;
|
||||
this.dasm = debug.disassembler;
|
||||
this.data = [];
|
||||
this.dataAddress = this.address;
|
||||
this.debug = debug;
|
||||
this.digit = null;
|
||||
this.isSubscribed = false;
|
||||
this.lines = [];
|
||||
this.selection = this.address;
|
||||
this.sim = debug.sim;
|
||||
|
||||
// Configure view
|
||||
let view = document.createElement("div");
|
||||
view.className = "tk tk-view";
|
||||
Object.assign(view.style, {
|
||||
display : "grid",
|
||||
gridTemplateColumns: "repeat(17, max-content)"
|
||||
});
|
||||
this.setView(view);
|
||||
|
||||
// Font-measuring element
|
||||
this.metrics = new Toolkit.Component(this.app, {
|
||||
className: "tk tk-metrics tk-mono",
|
||||
tagName : "div",
|
||||
style : {
|
||||
position : "absolute",
|
||||
visibility: "hidden"
|
||||
}
|
||||
});
|
||||
this.metrics.element.innerText = "X";
|
||||
this.append(this.metrics.element);
|
||||
|
||||
// Configure event handlers
|
||||
Toolkit.addResizeListener(this.viewport, e=>this.onResize(e));
|
||||
this.addEventListener("keydown" , e=>this.onKeyDown (e));
|
||||
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
|
||||
this.addEventListener("wheel" , e=>this.onMouseWheel (e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Typed a digit
|
||||
onDigit(digit) {
|
||||
|
||||
// Begin an edit
|
||||
if (this.digit === null) {
|
||||
this.digit = digit;
|
||||
this.setSelection(this.selection, true);
|
||||
}
|
||||
|
||||
// Complete an edit
|
||||
else {
|
||||
this.digit = this.digit << 4 | digit;
|
||||
this.setSelection(this.selection + 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
let key = e.key;
|
||||
|
||||
// A hex digit was entered
|
||||
if (key.toUpperCase() in DIGITS) {
|
||||
this.onDigit(DIGITS[key.toUpperCase()]);
|
||||
key = "digit";
|
||||
}
|
||||
|
||||
// Processing by key
|
||||
switch (key) {
|
||||
|
||||
// Arrow key navigation
|
||||
case "ArrowDown" : this.setSelection(this.selection + 16); break;
|
||||
case "ArrowLeft" : this.setSelection(this.selection - 1); break;
|
||||
case "ArrowRight": this.setSelection(this.selection + 1); break;
|
||||
case "ArrowUp" : this.setSelection(this.selection - 16); break;
|
||||
|
||||
// Commit current edit
|
||||
case "Enter":
|
||||
case " ":
|
||||
if (this.digit !== null)
|
||||
this.setSelection(this.selection);
|
||||
break;
|
||||
|
||||
// Goto
|
||||
case "g": case "G":
|
||||
if (!e.ctrlKey)
|
||||
return;
|
||||
this.promptGoto();
|
||||
break;
|
||||
|
||||
// Page key navigation
|
||||
case "PageDown":
|
||||
this.setSelection(this.selection + this.tall(false) * 16);
|
||||
break;
|
||||
case "PageUp":
|
||||
this.setSelection(this.selection - this.tall(false) * 16);
|
||||
break;
|
||||
|
||||
// Hex digit: already processed
|
||||
case "digit": break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Mouse wheel
|
||||
onMouseWheel(e) {
|
||||
|
||||
// User agent scaling action
|
||||
if (e.ctrlKey)
|
||||
return;
|
||||
|
||||
// No rotation has occurred
|
||||
let offset = Math.sign(e.deltaY) * 48;
|
||||
if (offset == 0)
|
||||
return;
|
||||
|
||||
// Update the display address
|
||||
this.address = Util.u32(this.address + offset);
|
||||
this.fetch(this.address, true);
|
||||
}
|
||||
|
||||
// Pointer down
|
||||
onPointerDown(e) {
|
||||
|
||||
// Common handling
|
||||
this.focus();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Not a click action
|
||||
if (e.button != 0)
|
||||
return;
|
||||
|
||||
// Determine the row that was clicked on
|
||||
let lineHeight = !this.metrics ? 0 :
|
||||
Math.max(1, Math.ceil(this.metrics.getBounds().height));
|
||||
let y = Math.floor((e.y - this.getBounds().top) / lineHeight);
|
||||
|
||||
// Determine the column that was clicked on
|
||||
let columns = this.lines[0].lblBytes;
|
||||
let bndCur = columns[0].getBoundingClientRect();
|
||||
if (e.x >= bndCur.left) for (let x = 0; x < 16; x++) {
|
||||
let bndNext = x == 15 ? null :
|
||||
columns[x + 1].getBoundingClientRect();
|
||||
|
||||
// The current column was clicked: update the selection
|
||||
if (e.x < (x == 15 ? bndCur.right :
|
||||
bndCur.right + (bndNext.left - bndCur.right) / 2)) {
|
||||
this.setSelection(this.address + y * 16 + x);
|
||||
return;
|
||||
}
|
||||
|
||||
// Advance to the next column
|
||||
bndCur = bndNext;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Viewport resized
|
||||
onResize(e) {
|
||||
let fetch = false;
|
||||
let tall = this.tall(true);
|
||||
|
||||
// Add additional lines to the output
|
||||
for (let x = 0; x < tall; x++) {
|
||||
if (x >= this.lines.length) {
|
||||
fetch = true;
|
||||
this.lines.push(new Line(this, x));
|
||||
}
|
||||
this.lines[x].setVisible(true);
|
||||
}
|
||||
|
||||
// Remove extra lines from the output
|
||||
for (let x = tall; x < this.lines.length; x++)
|
||||
this.lines[x].setVisible(false);
|
||||
|
||||
// Configure horizontal scroll bar
|
||||
if (this.metrics)
|
||||
this.horizontal.setIncrement(this.metrics.getBounds().width);
|
||||
|
||||
// Update the display
|
||||
if (fetch)
|
||||
this.fetch(this.address, true);
|
||||
else this.refresh();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Update with memory state from the core
|
||||
refresh(data) {
|
||||
|
||||
// Update with data from the core thread
|
||||
if (data) {
|
||||
this.data = data.bytes;
|
||||
this.dataAddress = data.address;
|
||||
}
|
||||
|
||||
// Update elements
|
||||
for (let y = 0, tall = this.tall(true); y < tall; y++)
|
||||
this.lines[y].refresh();
|
||||
}
|
||||
|
||||
// Subscribe to or unsubscribe from core updates
|
||||
setSubscribed(subscribed) {
|
||||
subscribed = !!subscribed;
|
||||
|
||||
// Nothing to change
|
||||
if (subscribed == this.isSubscribed)
|
||||
return;
|
||||
|
||||
// Configure instance fields
|
||||
this.isSubscribed = subscribed;
|
||||
|
||||
// Subscribe to core updates
|
||||
if (subscribed)
|
||||
this.fetch(this.address);
|
||||
|
||||
// Unsubscribe from core updates
|
||||
else this.sim.unsubscribe("memory");
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// The disassembler configuration has changed
|
||||
dasmChanged() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Retrieve memory data from the core
|
||||
async fetch(address, prefresh) {
|
||||
|
||||
// Configure instance fields
|
||||
this.address = address;
|
||||
|
||||
// Update the view immediately
|
||||
if (prefresh)
|
||||
this.refresh();
|
||||
|
||||
// Retrieve data from the core
|
||||
this.refresh(
|
||||
await this.sim.read(
|
||||
address - 16 * 16,
|
||||
(this.tall(true) + 32) * 16, {
|
||||
subscribe: this.isSubscribed && "memory"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Prompt the user to specify a new address
|
||||
promptGoto() {
|
||||
|
||||
// Receive input from the user
|
||||
let address = prompt(this.app.translate("common.gotoPrompt"));
|
||||
if (address == null)
|
||||
return;
|
||||
|
||||
// Process the input as an address in hexadecimal
|
||||
address = parseInt(address, 16);
|
||||
if (isNaN(address))
|
||||
return;
|
||||
|
||||
// The address is not currently visible in the output
|
||||
let tall = this.tall(false);
|
||||
if (Util.u32(address - this.address) >= tall * 16) {
|
||||
this.fetch(Util.u32(
|
||||
(address & 0xFFFFFFF0) - Math.floor(tall / 3) * 16));
|
||||
}
|
||||
|
||||
// Move the selection and refresh the display
|
||||
this.setSelection(Util.u32(address));
|
||||
}
|
||||
|
||||
// Specify which byte is selected
|
||||
setSelection(address, noCommit) {
|
||||
let fetch = false;
|
||||
|
||||
// Commit a pending data entry
|
||||
if (!noCommit && this.digit !== null) {
|
||||
this.write(this.digit);
|
||||
this.digit = null;
|
||||
fetch = true;
|
||||
}
|
||||
|
||||
// Configure instance fields
|
||||
this.selection = address = Util.u32(address);
|
||||
|
||||
// Working variables
|
||||
let row = Util.s32(address - this.address & 0xFFFFFFF0) / 16;
|
||||
|
||||
// The new address is above the top line of output
|
||||
if (row < 0) {
|
||||
this.fetch(Util.u32(this.address + row * 16), true);
|
||||
return;
|
||||
}
|
||||
|
||||
// The new address is below the bottom line of output
|
||||
let tall = this.tall(false);
|
||||
if (row >= tall) {
|
||||
this.fetch(Util.u32(address - tall * 16 + 16 & 0xFFFFFFF0), true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the display
|
||||
if (fetch)
|
||||
this.fetch(this.address, true);
|
||||
else this.refresh();
|
||||
}
|
||||
|
||||
// Measure how many rows of output are visible
|
||||
tall(partial) {
|
||||
let lineHeight = !this.metrics ? 0 :
|
||||
Math.ceil(this.metrics.getBounds().height);
|
||||
return lineHeight <= 0 ? 1 : Math.max(1, Math[partial?"ceil":"floor"](
|
||||
this.viewport.getBoundingClientRect().height / lineHeight));
|
||||
}
|
||||
|
||||
// Write a value to the core thread
|
||||
write(value) {
|
||||
this.data[Util.s32(this.selection - this.dataAddress)] = value;
|
||||
this.sim.write(
|
||||
this.selection,
|
||||
Uint8Array.from([ value ]), {
|
||||
refresh: true
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export { Memory };
|
|
@ -0,0 +1,863 @@
|
|||
import { Util } from /**/"./Util.js";
|
||||
|
||||
|
||||
|
||||
// Value types
|
||||
const HEX = 0;
|
||||
const SIGNED = 1;
|
||||
const UNSIGNED = 2;
|
||||
const FLOAT = 3;
|
||||
|
||||
// System register indexes
|
||||
const ADTRE = 25;
|
||||
const CHCW = 24;
|
||||
const ECR = 4;
|
||||
const EIPC = 0;
|
||||
const EIPSW = 1;
|
||||
const FEPC = 2;
|
||||
const FEPSW = 3;
|
||||
const PC = -1;
|
||||
const PIR = 6;
|
||||
const PSW = 5;
|
||||
const TKCW = 7;
|
||||
|
||||
// Program register names
|
||||
const PROREGS = {
|
||||
[ 2]: "hp",
|
||||
[ 3]: "sp",
|
||||
[ 4]: "gp",
|
||||
[ 5]: "tp",
|
||||
[31]: "lp"
|
||||
};
|
||||
|
||||
// System register names
|
||||
const SYSREGS = {
|
||||
[ADTRE]: "ADTRE",
|
||||
[CHCW ]: "CHCW",
|
||||
[ECR ]: "ECR",
|
||||
[EIPC ]: "EIPC",
|
||||
[EIPSW]: "EIPSW",
|
||||
[FEPC ]: "FEPC",
|
||||
[FEPSW]: "FEPSW",
|
||||
[PC ]: "PC",
|
||||
[PIR ]: "PIR",
|
||||
[PSW ]: "PSW",
|
||||
[TKCW ]: "TKCW",
|
||||
[29 ]: "29",
|
||||
[30 ]: "30",
|
||||
[31 ]: "31"
|
||||
};
|
||||
|
||||
// Expansion control types
|
||||
const BIT = 0;
|
||||
const INT = 1;
|
||||
|
||||
// Produce a template object for register expansion controls
|
||||
function ctrl(name, shift, size, disabled) {
|
||||
return {
|
||||
disabled: !!disabled,
|
||||
name : name,
|
||||
shift : shift,
|
||||
size : size
|
||||
};
|
||||
}
|
||||
|
||||
// Program register epansion controls
|
||||
const EXP_PROGRAM = [
|
||||
ctrl("cpu.hex" , true , HEX ),
|
||||
ctrl("cpu.signed" , false, SIGNED ),
|
||||
ctrl("cpu.unsigned", false, UNSIGNED),
|
||||
ctrl("cpu.float" , false, FLOAT )
|
||||
];
|
||||
|
||||
// CHCW expansion controls
|
||||
const EXP_CHCW = [
|
||||
ctrl("ICE", 1, 1)
|
||||
];
|
||||
|
||||
// ECR expansion controls
|
||||
const EXP_ECR = [
|
||||
ctrl("FECC", 16, 16),
|
||||
ctrl("EICC", 0, 16)
|
||||
];
|
||||
|
||||
// PIR expansion controls
|
||||
const EXP_PIR = [
|
||||
ctrl("PT", 0, 16, true)
|
||||
];
|
||||
|
||||
// PSW expansion controls
|
||||
const EXP_PSW = [
|
||||
ctrl("CY", 3, 1), ctrl("FRO", 9, 1),
|
||||
ctrl("OV", 2, 1), ctrl("FIV", 8, 1),
|
||||
ctrl("S" , 1, 1), ctrl("FZD", 7, 1),
|
||||
ctrl("Z" , 0, 1), ctrl("FOV", 6, 1),
|
||||
ctrl("NP", 15, 1), ctrl("FUD", 5, 1),
|
||||
ctrl("EP", 14, 1), ctrl("FPR", 4, 1),
|
||||
ctrl("ID", 12, 1), ctrl("I" , 16, 4),
|
||||
ctrl("AE", 13, 1)
|
||||
];
|
||||
|
||||
// TKCW expansion controls
|
||||
const EXP_TKCW = [
|
||||
ctrl("FIT", 7, 1, true), ctrl("FUT", 4, 1, true),
|
||||
ctrl("FZT", 6, 1, true), ctrl("FPT", 3, 1, true),
|
||||
ctrl("FVT", 5, 1, true), ctrl("OTM", 8, 1, true),
|
||||
ctrl("RDI", 2, 1, true), ctrl("RD" , 0, 2, true)
|
||||
];
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Register //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// One register within a register list
|
||||
class Register {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(list, index, andMask, orMask) {
|
||||
|
||||
// Configure instance fields
|
||||
this.andMask = andMask;
|
||||
this.app = list.app;
|
||||
this.controls = [];
|
||||
this.dasm = list.dasm;
|
||||
this.index = index;
|
||||
this.isExpanded = null;
|
||||
this.list = list;
|
||||
this.orMask = orMask;
|
||||
this.sim = list.sim;
|
||||
this.system = list.system;
|
||||
this.type = HEX;
|
||||
this.value = 0x00000000;
|
||||
|
||||
// Configure main controls
|
||||
this.contents = new Toolkit.Component(this.app, {
|
||||
className: "tk tk-contents",
|
||||
tagName : "div",
|
||||
style : {
|
||||
display : "grid",
|
||||
gridTemplateColumns: "max-content auto max-content"
|
||||
}
|
||||
});
|
||||
|
||||
// Processing by type
|
||||
this[this.system ? "initSystem" : "initProgram"]();
|
||||
|
||||
// Expansion button
|
||||
this.btnExpand = new Toolkit.Component(this.app, {
|
||||
className: "tk tk-expand tk-mono",
|
||||
tagName : "div"
|
||||
});
|
||||
this.list.view.append(this.btnExpand);
|
||||
|
||||
// Name label
|
||||
this.lblName = document.createElement("div");
|
||||
Object.assign(this.lblName, {
|
||||
className: "tk tk-name",
|
||||
id : Toolkit.id(),
|
||||
innerText: this.dasm.sysregCaps?this.name:this.name.toLowerCase()
|
||||
});
|
||||
this.lblName.style.userSelect = "none";
|
||||
this.list.view.append(this.lblName);
|
||||
|
||||
// Value text box
|
||||
this.txtValue = new Toolkit.TextBox(this.app, {
|
||||
className: "tk tk-textbox tk-mono",
|
||||
maxLength: 8
|
||||
});
|
||||
this.txtValue.setAttribute("aria-labelledby", this.lblName.id);
|
||||
this.txtValue.setAttribute("digits", "8");
|
||||
this.txtValue.addEventListener("action", e=>this.onValue());
|
||||
this.list.view.append(this.txtValue);
|
||||
|
||||
// Expansion area
|
||||
if (this.expansion != null)
|
||||
this.list.view.append(this.expansion);
|
||||
|
||||
// Enable expansion function
|
||||
if (this.expansion != null) {
|
||||
let key = e=>this.expandKeyDown (e);
|
||||
let pointer = e=>this.expandPointerDown(e);
|
||||
this.btnExpand.setAttribute("aria-controls", this.expansion.id);
|
||||
this.btnExpand.setAttribute("aria-labelledby", this.lblName.id);
|
||||
this.btnExpand.setAttribute("role", "button");
|
||||
this.btnExpand.setAttribute("tabindex", "0");
|
||||
this.btnExpand.addEventListener("keydown" , key );
|
||||
this.btnExpand.addEventListener("pointerdown", pointer);
|
||||
this.lblName .addEventListener("pointerdown", pointer);
|
||||
this.setExpanded(this.system && this.index == PSW);
|
||||
}
|
||||
|
||||
// Expansion function is unavailable
|
||||
else this.btnExpand.setAttribute("aria-hidden", "true");
|
||||
|
||||
}
|
||||
|
||||
// Set up a program register
|
||||
initProgram() {
|
||||
this.name = PROREGS[this.index] || "r" + this.index.toString();
|
||||
this.initExpansion(EXP_PROGRAM);
|
||||
}
|
||||
|
||||
// Set up a system register
|
||||
initSystem() {
|
||||
this.name = SYSREGS[this.index] || this.index.toString();
|
||||
|
||||
switch (this.index) {
|
||||
case CHCW :
|
||||
this.initExpansion(EXP_CHCW); break;
|
||||
case ECR :
|
||||
this.initExpansion(EXP_ECR ); break;
|
||||
case EIPSW: case FEPSW: case PSW:
|
||||
this.initExpansion(EXP_PSW ); break;
|
||||
case PIR :
|
||||
this.initExpansion(EXP_PIR ); break;
|
||||
case TKCW :
|
||||
this.initExpansion(EXP_TKCW); break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Initialize expansion controls
|
||||
initExpansion(controls) {
|
||||
let two = this.index == ECR || this.index == PIR;
|
||||
|
||||
// Establish expansion element
|
||||
let exp = this.expansion = new Toolkit.Component(this.app, {
|
||||
className: "tk tk-expansion",
|
||||
id : Toolkit.id(),
|
||||
tagName : "div",
|
||||
style : {
|
||||
display : "none",
|
||||
gridColumnEnd : "span 3",
|
||||
gridTemplateColumns:
|
||||
this.system ? "repeat(2, max-content)" : "max-content"
|
||||
}
|
||||
});
|
||||
|
||||
// Produce program register controls
|
||||
if (!this.system) {
|
||||
let group = new Toolkit.Group();
|
||||
exp.append(group);
|
||||
|
||||
// Process all controls
|
||||
for (let template of controls) {
|
||||
|
||||
// Create control
|
||||
let ctrl = new Toolkit.Radio(this.app, {
|
||||
group : group,
|
||||
selected: template.shift,
|
||||
text : template.name
|
||||
});
|
||||
ctrl.format = template.size;
|
||||
|
||||
// Configure event handler
|
||||
ctrl.addEventListener("action",
|
||||
e=>this.setType(e.component.format));
|
||||
|
||||
// Add the control to the element
|
||||
let box = document.createElement("div");
|
||||
box.append(ctrl.element);
|
||||
exp.append(box);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Process all control templates
|
||||
for (let template of controls) {
|
||||
let box, ctrl;
|
||||
|
||||
// Not using an inner two-column layout
|
||||
if (!two)
|
||||
exp.append(box = document.createElement("div"));
|
||||
|
||||
// Bit check box
|
||||
if (template.size == 1) {
|
||||
box.classList.add("tk-bit");
|
||||
|
||||
// Create control
|
||||
ctrl = new Toolkit.CheckBox(this.app, {
|
||||
text : "name",
|
||||
substitutions: { name: template.name }
|
||||
});
|
||||
ctrl.mask = 1 << template.shift;
|
||||
box.append(ctrl.element);
|
||||
|
||||
// Disable control
|
||||
if (template.disabled)
|
||||
ctrl.setEnabled(false);
|
||||
|
||||
// Configure event handler
|
||||
ctrl.addEventListener("action", e=>this.onBit(e.component));
|
||||
}
|
||||
|
||||
// Number text box
|
||||
else {
|
||||
if (!two)
|
||||
box.classList.add("tk-number");
|
||||
|
||||
// Create label
|
||||
let label = document.createElement("label");
|
||||
Object.assign(label, {
|
||||
className: "tk tk-label",
|
||||
innerText: template.name,
|
||||
});
|
||||
if (!two) Object.assign(box.style, {
|
||||
columnGap : "2px",
|
||||
display : "grid",
|
||||
gridTemplateColumns: "max-content auto"
|
||||
});
|
||||
(two ? exp : box).append(label);
|
||||
|
||||
// Create control
|
||||
ctrl = new Toolkit.TextBox(this.app, {
|
||||
id : Toolkit.id(),
|
||||
style: { height: "1em" }
|
||||
});
|
||||
label.htmlFor = ctrl.id;
|
||||
(two ? exp : box).append(ctrl.element);
|
||||
|
||||
// Control is a hex field
|
||||
if (template.size == 16) {
|
||||
ctrl.element.classList.add("tk-mono");
|
||||
ctrl.setAttribute("digits", 4);
|
||||
ctrl.setMaxLength(4);
|
||||
}
|
||||
|
||||
// Disable control
|
||||
if (template.disabled) {
|
||||
ctrl.setEnabled(false);
|
||||
(two ? label : box).setAttribute("disabled", "true");
|
||||
}
|
||||
|
||||
// Configure event handler
|
||||
ctrl.addEventListener("action", e=>this.onNumber(e.component));
|
||||
}
|
||||
|
||||
Object.assign(ctrl, template);
|
||||
this.controls.push(ctrl);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Expand button key press
|
||||
expandKeyDown(e) {
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
case "Enter":
|
||||
case " ":
|
||||
this.setExpanded(!this.isExpanded);
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Expand button pointer down
|
||||
expandPointerDown(e) {
|
||||
|
||||
// Focus management
|
||||
this.btnExpand.focus();
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0)
|
||||
return;
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Configure expansion area
|
||||
this.setExpanded(!this.isExpanded);
|
||||
}
|
||||
|
||||
// Expansion bit check box
|
||||
onBit(ctrl) {
|
||||
this.setValue(ctrl.isSelected ?
|
||||
this.value | ctrl.mask :
|
||||
this.value & Util.u32(~ctrl.mask)
|
||||
);
|
||||
}
|
||||
|
||||
// Expansion number text box
|
||||
onNumber(ctrl) {
|
||||
let mask = (1 << ctrl.size) - 1 << ctrl.shift;
|
||||
let value = parseInt(ctrl.getText(), ctrl.size == 16 ? 16 : 10);
|
||||
this.setValue(isNaN(value) ? this.value :
|
||||
this.value & Util.u32(~mask) | value << ctrl.shift & mask);
|
||||
}
|
||||
|
||||
// Register value
|
||||
onValue() {
|
||||
let text = this.txtValue.getText();
|
||||
let value;
|
||||
|
||||
// Processing by type
|
||||
switch (this.type) {
|
||||
|
||||
// Unsigned hexadecimal
|
||||
case HEX:
|
||||
value = parseInt(text, 16);
|
||||
break;
|
||||
|
||||
// Decimal
|
||||
case SIGNED:
|
||||
case UNSIGNED:
|
||||
value = parseInt(text);
|
||||
break;
|
||||
|
||||
// Float
|
||||
case FLOAT:
|
||||
value = parseFloat(text);
|
||||
if (isNaN(value))
|
||||
break;
|
||||
value = Util.fromF32(value);
|
||||
break;
|
||||
}
|
||||
|
||||
// Assign the new value
|
||||
this.setValue(isNaN(value) ? this.value : value);
|
||||
}
|
||||
|
||||
// Specify whether the expansion area is visible
|
||||
setExpanded(expanded) {
|
||||
expanded = !!expanded;
|
||||
|
||||
// Error checking
|
||||
if (this.expansion == null || expanded === this.isExpanded)
|
||||
return;
|
||||
|
||||
// Configure instance fields
|
||||
this.isExpanded = expanded;
|
||||
|
||||
// Configure elements
|
||||
let key = expanded ? "common.collapse" : "common.expand";
|
||||
this.btnExpand.setAttribute("aria-expanded", expanded);
|
||||
this.btnExpand.setToolTip(key);
|
||||
this.expansion.element.style.display =
|
||||
expanded ? "inline-grid" : "none";
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Disassembler settings have been updated
|
||||
dasmChanged() {
|
||||
let dasm = this.list.dasm;
|
||||
let name = this.name;
|
||||
|
||||
// Program register name
|
||||
if (!this.system) {
|
||||
if (!dasm.proregNames)
|
||||
name = "r" + this.index.toString();
|
||||
if (dasm.proregCaps)
|
||||
name = name.toUpperCase();
|
||||
}
|
||||
|
||||
// System register name
|
||||
else {
|
||||
if (!dasm.sysregCaps)
|
||||
name = name.toLowerCase();
|
||||
}
|
||||
|
||||
// Common processing
|
||||
this.lblName.innerText = name;
|
||||
this.refresh(this.value);
|
||||
}
|
||||
|
||||
// Update the value returned from the core
|
||||
refresh(value) {
|
||||
let text;
|
||||
|
||||
// Configure instance fields
|
||||
this.value = value;
|
||||
|
||||
// Value text box
|
||||
if (this.type == HEX) {
|
||||
this.txtValue.element.classList.add("tk-mono");
|
||||
this.txtValue.setMaxLength(8);
|
||||
} else {
|
||||
this.txtValue.element.classList.remove("tk-mono");
|
||||
this.txtValue.setMaxLength(null);
|
||||
}
|
||||
switch (this.type) {
|
||||
|
||||
// Unsigned hexadecimal
|
||||
case HEX:
|
||||
text = value.toString(16).padStart(8, "0");
|
||||
if (this.dasm.hexCaps)
|
||||
text = text.toUpperCase();
|
||||
break;
|
||||
|
||||
// Signed decimal
|
||||
case SIGNED:
|
||||
text = Util.s32(value).toString();
|
||||
break;
|
||||
|
||||
// Unsigned decial
|
||||
case UNSIGNED:
|
||||
text = Util.u32(value).toString();
|
||||
break;
|
||||
|
||||
// Float
|
||||
case FLOAT:
|
||||
if ((value & 0x7F800000) != 0x7F800000) {
|
||||
text = Util.toF32(value).toFixed(5).replace(/0+$/, "");
|
||||
if (text.endsWith("."))
|
||||
text += "0";
|
||||
} else text = "NaN";
|
||||
break;
|
||||
}
|
||||
this.txtValue.setText(text);
|
||||
|
||||
// No further processing for program registers
|
||||
if (!this.system)
|
||||
return;
|
||||
|
||||
// Process all expansion controls
|
||||
for (let ctrl of this.controls) {
|
||||
|
||||
// Bit check box
|
||||
if (ctrl.size == 1) {
|
||||
ctrl.setSelected(value & ctrl.mask);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Integer text box
|
||||
text = value >> ctrl.shift & (1 << ctrl.size) - 1;
|
||||
text = ctrl.size != 16 ? text.toString() :
|
||||
text.toString(16).padStart(4, "0");
|
||||
if (this.dasm.hexCaps)
|
||||
text = text.toUpperCase();
|
||||
ctrl.setText(text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Specify the formatting type of the register value
|
||||
setType(type) {
|
||||
if (type == this.type)
|
||||
return;
|
||||
this.type = type;
|
||||
this.refresh(this.value);
|
||||
}
|
||||
|
||||
// Specify a new value for the register
|
||||
async setValue(value) {
|
||||
|
||||
// Update the display with the new value immediately
|
||||
value = Util.u32(value & this.andMask | this.orMask);
|
||||
let matched = value == this.value;
|
||||
this.refresh(value);
|
||||
if (matched)
|
||||
return;
|
||||
|
||||
// Update the new value in the core
|
||||
let options = { refresh: true };
|
||||
this.refresh(await (
|
||||
!this.system ?
|
||||
this.sim.setProgramRegister(this.index, value, options) :
|
||||
this.index == PC ?
|
||||
this.sim.setProgramCounter ( value, options) :
|
||||
this.sim.setSystemRegister (this.index, value, options)
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// RegisterList //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Scrolling list of registers
|
||||
class RegisterList extends Toolkit.ScrollPane {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(debug, system) {
|
||||
super(debug.app, {
|
||||
className: "tk tk-scrollpane tk-reglist " +
|
||||
(system ? "tk-system" : "tk-program"),
|
||||
vertical : Toolkit.ScrollPane.ALWAYS
|
||||
});
|
||||
|
||||
// Configure instance fields
|
||||
this.app = debug.app;
|
||||
this.dasm = debug.disassembler;
|
||||
this.method = system?"getSystemRegisters":"getProgramRegisters";
|
||||
this.sim = debug.sim;
|
||||
this.subscription = system ? "sysregs" : "proregs";
|
||||
this.system = system;
|
||||
|
||||
// Configure view element
|
||||
this.setView(new Toolkit.Component(debug.app, {
|
||||
className: "tk tk-list",
|
||||
tagName : "div",
|
||||
style : {
|
||||
display : "grid",
|
||||
gridTemplateColumns: "max-content auto max-content"
|
||||
}
|
||||
}));
|
||||
|
||||
// Font-measuring element
|
||||
let text = "";
|
||||
for (let x = 0; x < 16; x++) {
|
||||
if (x != 0) text += "\n";
|
||||
let digit = x.toString(16);
|
||||
text += digit + "\n" + digit.toUpperCase();
|
||||
}
|
||||
this.metrics = new Toolkit.Component(this.app, {
|
||||
className: "tk tk-mono",
|
||||
tagName : "div",
|
||||
style : {
|
||||
position : "absolute",
|
||||
visibility: "hidden"
|
||||
}
|
||||
});
|
||||
this.metrics.element.innerText = text;
|
||||
this.metrics.addEventListener("resize", e=>this.onMetrics());
|
||||
this.viewport.append(this.metrics.element);
|
||||
|
||||
// Processing by type
|
||||
this[system ? "initSystem" : "initProgram"]();
|
||||
|
||||
// Configure component
|
||||
this.addEventListener("keydown", e=>this.onKeyDown (e));
|
||||
this.addEventListener("wheel" , e=>this.onMouseWheel(e));
|
||||
}
|
||||
|
||||
// Initialize a list of program registers
|
||||
initProgram() {
|
||||
this[0] = new Register(this, 0, 0x00000000, 0x00000000);
|
||||
for (let x = 1; x < 32; x++)
|
||||
this[x] = new Register(this, x, 0xFFFFFFFF, 0x00000000);
|
||||
}
|
||||
|
||||
// Initialie a list of system registers
|
||||
initSystem() {
|
||||
this[PC ] = new Register(this, PC , 0xFFFFFFFE, 0x00000000);
|
||||
this[PSW ] = new Register(this, PSW , 0x000FF3FF, 0x00000000);
|
||||
this[ADTRE] = new Register(this, ADTRE, 0xFFFFFFFE, 0x00000000);
|
||||
this[CHCW ] = new Register(this, CHCW , 0x00000002, 0x00000000);
|
||||
this[ECR ] = new Register(this, ECR , 0xFFFFFFFF, 0x00000000);
|
||||
this[EIPC ] = new Register(this, EIPC , 0xFFFFFFFE, 0x00000000);
|
||||
this[EIPSW] = new Register(this, EIPSW, 0x000FF3FF, 0x00000000);
|
||||
this[FEPC ] = new Register(this, FEPC , 0xFFFFFFFE, 0x00000000);
|
||||
this[FEPSW] = new Register(this, FEPSW, 0x000FF3FF, 0x00000000);
|
||||
this[PIR ] = new Register(this, PIR , 0x00000000, 0x00005346);
|
||||
this[TKCW ] = new Register(this, TKCW , 0x00000000, 0x000000E0);
|
||||
this[29 ] = new Register(this, 29 , 0xFFFFFFFF, 0x00000000);
|
||||
this[30 ] = new Register(this, 30 , 0x00000000, 0x00000004);
|
||||
this[31 ] = new Register(this, 31 , 0xFFFFFFFF, 0x00000000);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
this.vertical.setValue(this.vertical.value +
|
||||
this.vertical.increment);
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
this.horizontal.setValue(this.horizontal.value -
|
||||
this.horizontal.increment);
|
||||
break;
|
||||
case "ArrowRight":
|
||||
this.horizontal.setValue(this.horizontal.value +
|
||||
this.horizontal.increment);
|
||||
break;
|
||||
case "ArrowUp":
|
||||
this.vertical.setValue(this.vertical.value -
|
||||
this.vertical.increment);
|
||||
break;
|
||||
case "PageDown":
|
||||
this.vertical.setValue(this.vertical.value +
|
||||
this.vertical.extent);
|
||||
break;
|
||||
case "PageUp":
|
||||
this.vertical.setValue(this.vertical.value -
|
||||
this.vertical.extent);
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Metrics element resized
|
||||
onMetrics() {
|
||||
|
||||
// Error checking
|
||||
if (!this.metrics)
|
||||
return;
|
||||
|
||||
// Measure the dimensions of one hex character
|
||||
let bounds = this.metrics.getBounds();
|
||||
if (bounds.height <= 0)
|
||||
return;
|
||||
let width = Math.ceil(bounds.width);
|
||||
let height = Math.ceil(bounds.height / 32);
|
||||
|
||||
// Resize all monospaced text boxes
|
||||
for (let box of this.element
|
||||
.querySelectorAll(".tk-textbox[digits]")) {
|
||||
Object.assign(box.style, {
|
||||
height: height + "px",
|
||||
width : (parseInt(box.getAttribute("digits")) * width) + "px"
|
||||
});
|
||||
}
|
||||
|
||||
// Update scroll bars
|
||||
this.horizontal.setIncrement(height);
|
||||
this.vertical .setIncrement(height);
|
||||
}
|
||||
|
||||
// Mouse wheel
|
||||
onMouseWheel(e) {
|
||||
|
||||
// User agent scaling action
|
||||
if (e.ctrlKey)
|
||||
return;
|
||||
|
||||
// No rotation has occurred
|
||||
let offset = Math.sign(e.deltaY) * 3;
|
||||
if (offset == 0)
|
||||
return;
|
||||
|
||||
// Update the display address
|
||||
this.vertical.setValue(this.vertical.value +
|
||||
this.vertical.increment * offset);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Update with CPU state from the core
|
||||
refresh(registers) {
|
||||
|
||||
// System registers
|
||||
if (this.system) {
|
||||
this[ADTRE].refresh(registers.adtre);
|
||||
this[CHCW ].refresh(registers.chcw );
|
||||
this[ECR ].refresh(registers.ecr );
|
||||
this[EIPC ].refresh(registers.eipc );
|
||||
this[EIPSW].refresh(registers.eipsw);
|
||||
this[FEPC ].refresh(registers.fepc );
|
||||
this[FEPSW].refresh(registers.fepsw);
|
||||
this[PC ].refresh(registers.pc );
|
||||
this[PIR ].refresh(registers.pir );
|
||||
this[PSW ].refresh(registers.psw );
|
||||
this[TKCW ].refresh(registers.tkcw );
|
||||
this[29 ].refresh(registers[29] );
|
||||
this[30 ].refresh(registers[30] );
|
||||
this[31 ].refresh(registers[31] );
|
||||
}
|
||||
|
||||
// Program registers
|
||||
else for (let x = 0; x < 32; x++)
|
||||
this[x].refresh(registers[x]);
|
||||
}
|
||||
|
||||
// Subscribe to or unsubscribe from core updates
|
||||
setSubscribed(subscribed) {
|
||||
subscribed = !!subscribed;
|
||||
|
||||
// Nothing to change
|
||||
if (subscribed == this.isSubscribed)
|
||||
return;
|
||||
|
||||
// Configure instance fields
|
||||
this.isSubscribed = subscribed;
|
||||
|
||||
// Subscribe to core updates
|
||||
if (subscribed)
|
||||
this.fetch();
|
||||
|
||||
// Unsubscribe from core updates
|
||||
else this.sim.unsubscribe(this.subscription);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Disassembler settings have been updated
|
||||
dasmChanged() {
|
||||
if (this.system) {
|
||||
for (let key of Object.keys(SYSREGS))
|
||||
this[key].dasmChanged();
|
||||
} else {
|
||||
for (let key = 0; key < 32; key++)
|
||||
this[key].dasmChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the initial size of the register list
|
||||
getPreferredSize() {
|
||||
let ret = {
|
||||
height: 0,
|
||||
width : 0
|
||||
};
|
||||
|
||||
// Error checking
|
||||
if (!this.view)
|
||||
return ret;
|
||||
|
||||
// Measure the view element
|
||||
ret.width = this.view.element.scrollWidth;
|
||||
|
||||
// Locate the bottom of PSW
|
||||
if (this.system && this[PSW].expansion) {
|
||||
ret.height = this[PSW].expansion.getBounds().bottom -
|
||||
this.view.getBounds().top;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Retrieve CPU state from the core
|
||||
async fetch() {
|
||||
this.refresh(
|
||||
await this.sim[this.method]({
|
||||
subscribe: this.isSubscribed && this.subscription
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export { RegisterList };
|
|
@ -0,0 +1,44 @@
|
|||
let F32 = new Float32Array( 1);
|
||||
let S32 = new Int32Array (F32.buffer, 0, 1);
|
||||
let U32 = new Uint32Array (F32.buffer, 0, 1);
|
||||
|
||||
// Interpret a floating short as a 32-bit integer
|
||||
function fromF32(x) {
|
||||
F32[0] = x;
|
||||
return S32[0];
|
||||
}
|
||||
|
||||
// Interpret a 32-bit integer as a floating short
|
||||
function toF32(x) {
|
||||
S32[0] = x;
|
||||
return F32[0];
|
||||
}
|
||||
|
||||
// Represent a value as a signed 32-bit integer
|
||||
function s32(x) {
|
||||
S32[0] = x;
|
||||
return S32[0];
|
||||
}
|
||||
|
||||
// Sign-extend a value with a given number of bits
|
||||
function signExtend(value, bits) {
|
||||
bits = 32 - bits;
|
||||
S32[0] = value << bits;
|
||||
return S32[0] >> bits;
|
||||
}
|
||||
|
||||
// Represent a value as an unsigned 32-bit integer
|
||||
function u32(x) {
|
||||
U32[0] = x;
|
||||
return U32[0];
|
||||
}
|
||||
|
||||
|
||||
|
||||
export let Util = {
|
||||
fromF32 : fromF32,
|
||||
toF32 : toF32,
|
||||
s32 : s32,
|
||||
signExtend: signExtend,
|
||||
u32 : u32
|
||||
};
|
|
@ -0,0 +1,196 @@
|
|||
import { Sim } from /**/"./Sim.js";
|
||||
|
||||
let url = u=>u.startsWith("data:")?u:new URL(u,import.meta.url).toString();
|
||||
|
||||
let RESTRICT = {};
|
||||
let WASM_URL = url(/**/"./core.wasm" );
|
||||
let WORKER_URL = url(/**/"./CoreWorker.js");
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Core //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Environment manager for simulated Virtual Boys
|
||||
class Core {
|
||||
|
||||
//////////////////////////////// Constants ////////////////////////////////
|
||||
|
||||
// States
|
||||
static IDLE = 0;
|
||||
static RUNNING = 1;
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Static Methods //////////////////////////////
|
||||
|
||||
// Create a new instance of Core
|
||||
static create(options) {
|
||||
return new Core(RESTRICT).init(options);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
// Stub constructor
|
||||
constructor(restrict) {
|
||||
if (restrict != RESTRICT) {
|
||||
throw "Cannot instantiate Core directly. " +
|
||||
"Use Core.create() instead.";
|
||||
}
|
||||
}
|
||||
|
||||
// Substitute constructor
|
||||
async init(options = {}) {
|
||||
|
||||
// Configure instance fields
|
||||
this.length = 0;
|
||||
this.onsubscriptions = null;
|
||||
this.resolutions = [];
|
||||
this.state = Core.IDLE;
|
||||
this.worker = new Worker(WORKER_URL);
|
||||
this.worker.onmessage = e=>this.onMessage(e.data);
|
||||
|
||||
// Issue a create command
|
||||
if ("sims" in options)
|
||||
await this.create(options.sims, WASM_URL);
|
||||
|
||||
// Only initialize the WebAssembly module
|
||||
else this.send("init", false, { wasm: WASM_URL });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Worker message received
|
||||
onMessage(data) {
|
||||
|
||||
// Process a promised response
|
||||
if ("response" in data)
|
||||
this.resolutions.shift()(data.response);
|
||||
|
||||
// Process subscriptions
|
||||
if (this.onsubscriptions && data.subscriptions)
|
||||
this.onsubscriptions(data.subscriptions);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Associate two simulations as peers, or remove an association
|
||||
connect(a, b, options = {}) {
|
||||
return this.send({
|
||||
command: "connect",
|
||||
respond: !("respond" in options) || !!options.respond,
|
||||
sims : [ a, b ]
|
||||
});
|
||||
}
|
||||
|
||||
// Create and initialize new simulations
|
||||
async create(sims, wasm) {
|
||||
let numSims = sims===undefined ? 1 : Math.max(0, parseInt(sims) || 0);
|
||||
|
||||
// Execute the command in the core thread
|
||||
let response = await this.send({
|
||||
command: "create",
|
||||
sims : numSims,
|
||||
wasm : wasm
|
||||
});
|
||||
|
||||
// Process the core thread's response
|
||||
let ret = [];
|
||||
for (let x = 0; x < numSims; x++, this.length++)
|
||||
ret.push(this[this.length] =
|
||||
new Sim(this, response[x], this.length));
|
||||
return sims === undefined ? ret[0] : ret;
|
||||
}
|
||||
|
||||
// Delete a simulation
|
||||
destroy(sim, options = {}) {
|
||||
|
||||
// Configure simulation
|
||||
sim = this[sim] || sim;
|
||||
if (sim.core != this)
|
||||
return;
|
||||
let ptr = sim.destroy();
|
||||
|
||||
// State management
|
||||
for (let x = sim.index + 1; x < this.length; x++)
|
||||
(this[x - 1] = this[x]).index--;
|
||||
delete this[--this.length];
|
||||
|
||||
// Execute the command on the core thread
|
||||
return this.send({
|
||||
command: "destroy",
|
||||
respond: !("respond" in options) || !!options.respond,
|
||||
sim : ptr
|
||||
});
|
||||
}
|
||||
|
||||
// Attempt to run until the next instruction
|
||||
runNext(a, b, options = {}) {
|
||||
return this.send({
|
||||
command: "runNext",
|
||||
refresh: !!options.refresh,
|
||||
respond: !("respond" in options) || !!options.respond,
|
||||
sims : [ a, b ]
|
||||
});
|
||||
}
|
||||
|
||||
// Execute one instruction
|
||||
singleStep(a, b, options = {}) {
|
||||
return this.send({
|
||||
command: "singleStep",
|
||||
refresh: !!options.refresh,
|
||||
respond: !("respond" in options) || !!options.respond,
|
||||
sims : [ a, b ]
|
||||
});
|
||||
}
|
||||
|
||||
// Unsubscribe from frame data
|
||||
unsubscribe(key, sim = 0) {
|
||||
this.send({
|
||||
command: "unsubscribe",
|
||||
key : key,
|
||||
respond: false,
|
||||
sim : sim
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Send a message to the Worker
|
||||
send(data = {}, transfers = []) {
|
||||
|
||||
// Create the message object
|
||||
Object.assign(data, {
|
||||
respond: !("respond" in data) || !!data.respond,
|
||||
run : !("run" in data) || !!data.run
|
||||
});
|
||||
|
||||
// Do not wait on a response
|
||||
if (!data.respond)
|
||||
this.worker.postMessage(data, transfers);
|
||||
|
||||
// Wait for the response to come back
|
||||
else return new Promise((resolve, reject)=>{
|
||||
this.resolutions.push(response=>resolve(response));
|
||||
this.worker.postMessage(data, transfers);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export { Core };
|
|
@ -0,0 +1,378 @@
|
|||
"use strict";
|
||||
|
||||
|
||||
|
||||
// Un-sign a 32-bit integer
|
||||
// Emscripten is sign-extending uint32_t and Firefox can't import in Workers
|
||||
let u32 = (()=>{
|
||||
let U32 = new Uint32Array(1);
|
||||
return x=>{ U32[0] = x; return U32[0]; };
|
||||
})();
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// CoreWorker //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Thread manager for Core commands
|
||||
new class CoreWorker {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
// Stub constructor
|
||||
constructor() {
|
||||
onmessage = async e=>{
|
||||
await this.init(e.data.wasm);
|
||||
onmessage = e=>this.onCommand(e.data, false);
|
||||
onmessage(e);
|
||||
};
|
||||
}
|
||||
|
||||
// Substitute constructor
|
||||
async init(wasm) {
|
||||
|
||||
// Load the WebAssembly module
|
||||
let imports = {
|
||||
env: { emscripten_notify_memory_growth: ()=>this.onMemory() }
|
||||
};
|
||||
this.wasm = await (typeof wasm == "string" ?
|
||||
WebAssembly.instantiateStreaming(fetch(wasm), imports) :
|
||||
WebAssembly.instantiate ( wasm , imports)
|
||||
);
|
||||
|
||||
// Configure instance fields
|
||||
this.api = this.wasm.instance.exports;
|
||||
this.frameData = null;
|
||||
this.isRunning = false;
|
||||
this.memory = this.api.memory.buffer;
|
||||
this.ptrSize = this.api.PointerSize();
|
||||
this.ptrType = this.ptrSize == 4 ? Uint32Array : Uint64Array;
|
||||
this.subscriptions = {};
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Message from audio thread
|
||||
onAudio(frames) {
|
||||
}
|
||||
|
||||
// Message from main thread
|
||||
onCommand(data) {
|
||||
|
||||
// Subscribe to the command
|
||||
if (data.subscribe) {
|
||||
let sub = data.sim || 0;
|
||||
sub = this.subscriptions[sub] || (this.subscriptions[sub] = {});
|
||||
sub = sub[data.subscribe] = {};
|
||||
Object.assign(sub, data);
|
||||
delete sub.promised;
|
||||
delete sub.run;
|
||||
delete sub.subscribe;
|
||||
}
|
||||
|
||||
// Execute the command
|
||||
if (data.run)
|
||||
this[data.command](data);
|
||||
|
||||
// Process all subscriptions to refresh any debugging interfaces
|
||||
if (data.refresh)
|
||||
this.doSubscriptions(data.sim ? [ data.sim ] : data.sims);
|
||||
|
||||
// Reply to the main thread
|
||||
if (data.respond) {
|
||||
postMessage({
|
||||
response: data.response
|
||||
}, data.transfers);
|
||||
}
|
||||
}
|
||||
|
||||
// Memory growth
|
||||
onMemory() {
|
||||
this.memory = this.api.memory.buffer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////// Commands /////////////////////////////////
|
||||
|
||||
// Associate two simulations as peers, or remove an association
|
||||
connect(data) {
|
||||
this.api.vbConnect(data.sims[0], data.sims[1]);
|
||||
}
|
||||
|
||||
// Allocate and initialize a new simulation
|
||||
create(data) {
|
||||
let ptr = this.api.Create(data.sims);
|
||||
data.response = new this.ptrType(this.memory, ptr, data.sims).slice();
|
||||
data.transfers = [ data.response.buffer ];
|
||||
this.api.Free(ptr);
|
||||
}
|
||||
|
||||
// Delete a simulation
|
||||
destroy(data) {
|
||||
this.api.Destroy(data.sim);
|
||||
}
|
||||
|
||||
// Locate instructions for disassembly
|
||||
disassemble(data) {
|
||||
let decode; // Address of next row
|
||||
let index; // Index in list of next row
|
||||
let rows = new Array(data.rows);
|
||||
let pc = u32(this.api.vbGetProgramCounter(data.sim));
|
||||
let row; // Located output row
|
||||
|
||||
// The target address is before or on the first row of output
|
||||
if (data.row <= 0) {
|
||||
decode = u32(data.target - 4 * Math.max(0, data.row + 10));
|
||||
|
||||
// Locate the target row
|
||||
for (;;) {
|
||||
row = this.dasmRow(data.sim, decode, pc);
|
||||
if (u32(data.target - decode) < row.size)
|
||||
break;
|
||||
decode = u32(decode + row.size);
|
||||
}
|
||||
|
||||
// Locate the first row of output
|
||||
for (index = data.row; index < 0; index++) {
|
||||
decode = u32(decode + row.size);
|
||||
row = this.dasmRow(data.sim, decode, pc);
|
||||
}
|
||||
|
||||
// Prepare to process remaining rows
|
||||
decode = u32(decode + row.size);
|
||||
rows[0] = row;
|
||||
index = 1;
|
||||
}
|
||||
|
||||
// The target address is after the first row of output
|
||||
else {
|
||||
let circle = new Array(data.row + 1);
|
||||
let count = Math.min(data.row + 1, data.rows);
|
||||
let src = 0;
|
||||
decode = u32(data.target - 4 * (data.row + 10));
|
||||
|
||||
// Locate the target row
|
||||
for (;;) {
|
||||
row = circle[src] = this.dasmRow(data.sim, decode, pc);
|
||||
decode = u32(decode + row.size);
|
||||
if (u32(data.target - row.address) < row.size)
|
||||
break;
|
||||
src = (src + 1) % circle.length;
|
||||
}
|
||||
|
||||
// Copy entries from the circular buffer to the output list
|
||||
for (index = 0; index < count; index++) {
|
||||
src = (src + 1) % circle.length;
|
||||
rows[index] = circle[src];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Locate any remaining rows
|
||||
for (; index < data.rows; index++) {
|
||||
let row = rows[index] = this.dasmRow(data.sim, decode, pc);
|
||||
decode = u32(decode + row.size);
|
||||
}
|
||||
|
||||
// Respond to main thread
|
||||
data.response = {
|
||||
pc : pc,
|
||||
rows : rows,
|
||||
scroll: data.scroll
|
||||
};
|
||||
}
|
||||
|
||||
// Retrieve all CPU program registers
|
||||
getProgramRegisters(data) {
|
||||
let ret = data.response = new Uint32Array(32);
|
||||
for (let x = 0; x < 32; x++)
|
||||
ret[x] = this.api.vbGetProgramRegister(data.sim, x);
|
||||
data.transfers = [ ret.buffer ];
|
||||
}
|
||||
|
||||
// Retrieve the value of a system register
|
||||
getSystemRegister(data) {
|
||||
data.response = u32(this.api.vbGetSystemRegister(data.sim, data.id));
|
||||
}
|
||||
|
||||
// Retrieve all CPU system registers (including PC)
|
||||
getSystemRegisters(data) {
|
||||
data.response = {
|
||||
adtre: u32(this.api.vbGetSystemRegister(data.sim, 25)),
|
||||
chcw : u32(this.api.vbGetSystemRegister(data.sim, 24)),
|
||||
ecr : u32(this.api.vbGetSystemRegister(data.sim, 4)),
|
||||
eipc : u32(this.api.vbGetSystemRegister(data.sim, 0)),
|
||||
eipsw: u32(this.api.vbGetSystemRegister(data.sim, 1)),
|
||||
fepc : u32(this.api.vbGetSystemRegister(data.sim, 2)),
|
||||
fepsw: u32(this.api.vbGetSystemRegister(data.sim, 3)),
|
||||
pc : u32(this.api.vbGetProgramCounter(data.sim )),
|
||||
pir : u32(this.api.vbGetSystemRegister(data.sim, 6)),
|
||||
psw : u32(this.api.vbGetSystemRegister(data.sim, 5)),
|
||||
tkcw : u32(this.api.vbGetSystemRegister(data.sim, 7)),
|
||||
[29] : u32(this.api.vbGetSystemRegister(data.sim, 29)),
|
||||
[30] : u32(this.api.vbGetSystemRegister(data.sim, 30)),
|
||||
[31] : u32(this.api.vbGetSystemRegister(data.sim, 31))
|
||||
};
|
||||
}
|
||||
|
||||
// Read bytes from the simulation
|
||||
read(data) {
|
||||
let ptr = this.api.Malloc(data.length);
|
||||
this.api.ReadBuffer(data.sim, ptr, data.address, data.length);
|
||||
let buffer = new Uint8Array(this.memory, ptr, data.length).slice();
|
||||
this.api.Free(ptr);
|
||||
data.response = {
|
||||
address: data.address,
|
||||
bytes : buffer
|
||||
};
|
||||
data.transfers = [ buffer.buffer ];
|
||||
}
|
||||
|
||||
// Attempt to execute until the following instruction
|
||||
runNext(data) {
|
||||
this.api.RunNext(data.sims[0], data.sims[1]);
|
||||
let pc = [ u32(this.api.vbGetProgramCounter(data.sims[0])) ];
|
||||
if (data.sims[1])
|
||||
pc.push(u32(this.api.vbGetProgramCounter(data.sims[1])));
|
||||
data.response = {
|
||||
pc: pc
|
||||
}
|
||||
}
|
||||
|
||||
// Specify a new value for PC
|
||||
setProgramCounter(data) {
|
||||
data.response =
|
||||
u32(this.api.vbSetProgramCounter(data.sim, data.value));
|
||||
}
|
||||
|
||||
// Specify a new value for a program register
|
||||
setProgramRegister(data) {
|
||||
data.response = this.api.vbSetProgramRegister
|
||||
(data.sim, data.id, data.value);
|
||||
}
|
||||
|
||||
// Specify a ROM buffer
|
||||
setROM(data) {
|
||||
let ptr = this.api.Malloc(data.rom.length);
|
||||
let buffer = new Uint8Array(this.memory, ptr, data.rom.length);
|
||||
for (let x = 0; x < data.rom.length; x++)
|
||||
buffer[x] = data.rom[x];
|
||||
data.response = !!this.api.SetROM(data.sim, ptr, data.rom.length);
|
||||
}
|
||||
|
||||
// Specify a new value for a system register
|
||||
setSystemRegister(data) {
|
||||
data.response = u32(this.api.vbSetSystemRegister
|
||||
(data.sim, data.id, data.value));
|
||||
}
|
||||
|
||||
// Execute one instruction
|
||||
singleStep(data) {
|
||||
this.api.SingleStep(data.sims[0], data.sims[1]);
|
||||
let pc = [ u32(this.api.vbGetProgramCounter(data.sims[0])) ];
|
||||
if (data.sims[1])
|
||||
pc.push(u32(this.api.vbGetProgramCounter(data.sims[1])));
|
||||
data.response = {
|
||||
pc: pc
|
||||
}
|
||||
}
|
||||
|
||||
// Unsubscribe from frame data
|
||||
unsubscribe(data) {
|
||||
let sim = data.sim || 0;
|
||||
if (sim in this.subscriptions) {
|
||||
let subs = this.subscriptions[sim];
|
||||
delete subs[data.key];
|
||||
if (Object.keys(subs).length == 0)
|
||||
delete this.subscriptions[sim];
|
||||
}
|
||||
}
|
||||
|
||||
// Write bytes to the simulation
|
||||
write(data) {
|
||||
let ptr = this.api.Malloc(data.bytes.length);
|
||||
let buffer = new Uint8Array(this.memory, ptr, data.bytes.length);
|
||||
for (let x = 0; x < data.bytes.length; x++)
|
||||
buffer[x] = data.bytes[x];
|
||||
this.api.WriteBuffer(data.sim, ptr, data.address, data.bytes.length);
|
||||
this.api.Free(ptr);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Retrieve basic information for a row of disassembly
|
||||
dasmRow(sim, address, pc) {
|
||||
let bits = this.api.vbRead(sim, address, 3 /* VB_U16 */);
|
||||
let opcode = bits >> 10 & 63;
|
||||
let size = (
|
||||
opcode < 0b101000 || // Formats I through III
|
||||
opcode == 0b110010 || // Illegal
|
||||
opcode == 0b110110 // Illegal
|
||||
) ? 2 : 4;
|
||||
|
||||
// Establish row information
|
||||
let row = {
|
||||
address: address,
|
||||
bytes : [ bits & 0xFF, bits >> 8 ],
|
||||
size : u32(address + 2) == pc ? 2 : size
|
||||
//size : Math.min(u32(pc - address), size)
|
||||
};
|
||||
|
||||
// Read additional bytes
|
||||
if (size == 4) {
|
||||
bits = this.api.vbRead(sim, address + 2, 3 /* VB_U16 */);
|
||||
row.bytes.push(bits & 0xFF, bits >> 8);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
// Process subscriptions and send a message to the main thread
|
||||
doSubscriptions(sims) {
|
||||
let message = { subscriptions: {} };
|
||||
let transfers = [];
|
||||
|
||||
// Process all simulations
|
||||
for (let sim of sims) {
|
||||
|
||||
// There are no subscriptions for this sim
|
||||
if (!(sim in this.subscriptions))
|
||||
continue;
|
||||
|
||||
// Working variables
|
||||
let subs = message.subscriptions[sim] = {};
|
||||
|
||||
// Process all subscriptions
|
||||
for (let sub of Object.entries(this.subscriptions[sim])) {
|
||||
|
||||
// Run the command
|
||||
this[sub[1].command](sub[1]);
|
||||
|
||||
// Add the response to the message
|
||||
if (sub[1].response) {
|
||||
subs[sub[0]] = sub[1].response;
|
||||
delete sub[1].response;
|
||||
}
|
||||
|
||||
// Add the transferable objects to the message
|
||||
if (sub[1].transfers) {
|
||||
transfers.push(... sub[1].transfers);
|
||||
delete sub[1].transfers;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Send the message to the main thread
|
||||
if (Object.keys(message).length != 0)
|
||||
postMessage(message, transfers);
|
||||
}
|
||||
|
||||
}();
|
|
@ -0,0 +1,151 @@
|
|||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Sim //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// One simulated Virtual Boy
|
||||
class Sim {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(core, sim, index) {
|
||||
this.core = core;
|
||||
this.index = index;
|
||||
this.sim = sim;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Locate CPU instructions
|
||||
disassemble(target, row, rows, scroll, options = {}) {
|
||||
return this.core.send({
|
||||
command : "disassemble",
|
||||
row : row,
|
||||
rows : rows,
|
||||
scroll : scroll,
|
||||
sim : this.sim,
|
||||
subscribe: options.subscribe,
|
||||
target : target
|
||||
});
|
||||
}
|
||||
|
||||
// Retrieve all CPU program registers
|
||||
getProgramRegisters(options = {}) {
|
||||
return this.core.send({
|
||||
command : "getProgramRegisters",
|
||||
sim : this.sim,
|
||||
subscribe: options.subscribe
|
||||
});
|
||||
}
|
||||
|
||||
// Retrieve the value of a system register
|
||||
getSystemRegister(id, options = {}) {
|
||||
return this.core.send({
|
||||
command : "getSystemRegister",
|
||||
id : id,
|
||||
sim : this.sim,
|
||||
subscribe: options.subscribe
|
||||
});
|
||||
}
|
||||
|
||||
// Retrieve all CPU system registers (including PC)
|
||||
getSystemRegisters(options = {}) {
|
||||
return this.core.send({
|
||||
command : "getSystemRegisters",
|
||||
sim : this.sim,
|
||||
subscribe: options.subscribe
|
||||
});
|
||||
}
|
||||
|
||||
// Read multiple bytes from the bus
|
||||
read(address, length, options = {}) {
|
||||
return this.core.send({
|
||||
address : address,
|
||||
command : "read",
|
||||
debug : !("debug" in options) || !!options.debug,
|
||||
length : length,
|
||||
sim : this.sim,
|
||||
subscribe: options.subscribe
|
||||
});
|
||||
}
|
||||
|
||||
// Specify a new value for PC
|
||||
setProgramCounter(value, options = {}) {
|
||||
return this.core.send({
|
||||
command: "setProgramCounter",
|
||||
refresh: !!options.refresh,
|
||||
sim : this.sim,
|
||||
value : value
|
||||
});
|
||||
}
|
||||
|
||||
// Specify a new value for a program register
|
||||
setProgramRegister(id, value, options = {}) {
|
||||
return this.core.send({
|
||||
command: "setProgramRegister",
|
||||
id : id,
|
||||
refresh: !!options.refresh,
|
||||
sim : this.sim,
|
||||
value : value
|
||||
});
|
||||
}
|
||||
|
||||
// Specify the current ROM buffer
|
||||
setROM(rom, options = {}) {
|
||||
return this.core.send({
|
||||
command: "setROM",
|
||||
rom : rom,
|
||||
refresh: !!options.refresh,
|
||||
sim : this.sim
|
||||
}, [ rom.buffer ]);
|
||||
}
|
||||
|
||||
// Specify a new value for a system register
|
||||
setSystemRegister(id, value, options = {}) {
|
||||
return this.core.send({
|
||||
command: "setSystemRegister",
|
||||
id : id,
|
||||
refresh: !!options.refresh,
|
||||
sim : this.sim,
|
||||
value : value
|
||||
});
|
||||
}
|
||||
|
||||
// Ubsubscribe from frame data
|
||||
unsubscribe(key) {
|
||||
return this.core.unsubscribe(key, this.sim);
|
||||
}
|
||||
|
||||
// Write multiple bytes to the bus
|
||||
write(address, bytes, options = {}) {
|
||||
return this.core.send({
|
||||
address : address,
|
||||
command : "write",
|
||||
bytes : bytes,
|
||||
debug : !("debug" in options) || !!options.debug,
|
||||
refresh : !!options.refresh,
|
||||
sim : this.sim,
|
||||
subscribe: options.subscribe
|
||||
},
|
||||
bytes instanceof ArrayBuffer ? [ bytes ] :
|
||||
bytes.buffer instanceof ArrayBuffer ? [ bytes.buffer ] :
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// The simulation has been destroyed
|
||||
destroy() {
|
||||
let sim = this.sim;
|
||||
this.core = null;
|
||||
this.sim = 0;
|
||||
return sim;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Sim };
|
|
@ -1,44 +0,0 @@
|
|||
{
|
||||
key : "en-US",
|
||||
name: "English (United States)",
|
||||
app : {
|
||||
close : "Close",
|
||||
console : "Console",
|
||||
goto_ : "Enter the address to seek to:",
|
||||
romLoaded : "Successfully loaded file \"{filename}\" ({size})",
|
||||
romNotVB : "The selected file is not a Virtual Boy ROM.",
|
||||
readFileError: "Unable to read the selected file."
|
||||
},
|
||||
cpu: {
|
||||
_ : "CPU",
|
||||
disassembler: "Disassembler",
|
||||
float_ : "Float",
|
||||
hex : "Hex",
|
||||
pcFrom : "From",
|
||||
pcTo : "To",
|
||||
mainSplit : "Disassembler and registers splitter",
|
||||
regsSplit : "System registers and program registers splitter",
|
||||
signed : "Signed",
|
||||
unsigned : "Unsigned"
|
||||
},
|
||||
memory: {
|
||||
_ : "Memory",
|
||||
hexEditor: "Hex viewer"
|
||||
},
|
||||
menu: {
|
||||
_ : "Main application menu",
|
||||
debug: {
|
||||
_: "Debug",
|
||||
},
|
||||
file : {
|
||||
_ : "File",
|
||||
loadROM: "Load ROM..."
|
||||
},
|
||||
theme: {
|
||||
_ : "Theme",
|
||||
dark : "Dark",
|
||||
light : "Light",
|
||||
virtual: "Virtual"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"id": "en-US",
|
||||
|
||||
"common": {
|
||||
"close" : "Close",
|
||||
"collapse" : "Collapse",
|
||||
"expand" : "Expand",
|
||||
"gotoPrompt": "Enter the address to go to:"
|
||||
},
|
||||
|
||||
"error": {
|
||||
"fileRead": "An error occurred when reading the file.",
|
||||
"romNotVB": "The selected file is not a Virtual Boy ROM."
|
||||
},
|
||||
|
||||
"app": {
|
||||
"title": "Virtual Boy Emulator",
|
||||
|
||||
"menu": {
|
||||
"_": "Application menu bar",
|
||||
|
||||
"file": {
|
||||
"_" : "File",
|
||||
"loadROM" : "Load ROM{sim}...",
|
||||
"debugMode": "Debug mode"
|
||||
},
|
||||
|
||||
"emulation": {
|
||||
"_" : "Emulation",
|
||||
"run" : "Run",
|
||||
"reset" : "Reset",
|
||||
"dualSims": "Dual sims",
|
||||
"linkSims": "Link sims"
|
||||
},
|
||||
|
||||
"debug": {
|
||||
"_" : "Debug{sim}",
|
||||
"console" : "Console",
|
||||
"memory" : "Memory",
|
||||
"cpu" : "CPU",
|
||||
"breakpoints" : "Breakpoints",
|
||||
"palettes" : "Palettes",
|
||||
"characters" : "Characters",
|
||||
"bgMaps" : "BG maps",
|
||||
"objects" : "Objects",
|
||||
"worlds" : "Worlds",
|
||||
"frameBuffers": "Frame buffers"
|
||||
},
|
||||
|
||||
"theme": {
|
||||
"_" : "Theme",
|
||||
"light" : "Light",
|
||||
"dark" : "Dark",
|
||||
"virtual": "Virtual"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
"cpu": {
|
||||
"_" : "CPU{sim}",
|
||||
"float" : "Float",
|
||||
"hex" : "Hex",
|
||||
"signed" : "Signed",
|
||||
"splitHorizontal": "Program and system registers splitter",
|
||||
"splitVertical" : "Disassembler and registers splitter",
|
||||
"unsigned" : "Unsigned"
|
||||
},
|
||||
|
||||
"memory": {
|
||||
"_": "Memory{sim}"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// Global theme assets
|
||||
Bundle["app/theme/kiosk.css"].installStylesheet(true);
|
||||
await Bundle["app/theme/inconsolata.woff2"].installFont(
|
||||
"Inconsolata SemiExpanded Medium");
|
||||
await Bundle["app/theme/roboto.woff2"].installFont("Roboto");
|
||||
Bundle["app/theme/check.svg" ].installImage("tk-check" , "check.svg" );
|
||||
Bundle["app/theme/close.svg" ].installImage("tk-close" , "close.svg" );
|
||||
Bundle["app/theme/collapse.svg"].installImage("tk-collapse", "collapse.svg");
|
||||
Bundle["app/theme/expand.svg" ].installImage("tk-expand" , "expand.svg" );
|
||||
Bundle["app/theme/radio.svg" ].installImage("tk-radio" , "radio.svg" );
|
||||
Bundle["app/theme/scroll.svg" ].installImage("tk-scroll" , "scroll.svg" );
|
||||
|
||||
// Module imports
|
||||
import { Core } from /**/"./core/Core.js";
|
||||
import { Toolkit } from /**/"./toolkit/Toolkit.js";
|
||||
import { App } from /**/"./app/App.js";
|
||||
|
||||
// Begin application
|
||||
let dark = matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
let url = u=>u.startsWith("data:")?u:new URL(u,import.meta.url).toString();
|
||||
new App({
|
||||
core : await Core.create({ sims: 2 }),
|
||||
locale : navigator.language,
|
||||
standalone: true,
|
||||
theme : dark ? "dark" : "light",
|
||||
locales : [
|
||||
await (await fetch(url(/**/"./locale/en-US.json"))).json()
|
||||
],
|
||||
themes : {
|
||||
dark : Bundle["app/theme/dark.css" ].installStylesheet( dark),
|
||||
light : Bundle["app/theme/light.css" ].installStylesheet(!dark),
|
||||
virtual: Bundle["app/theme/virtual.css"].installStylesheet(false)
|
||||
}
|
||||
});
|
|
@ -3,8 +3,10 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Virtual Boy Emulator</title>
|
||||
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
|
||||
<script>window.a=async(b,c,d,e)=>{e=document.createElement('canvas');e.width=c;e.height=d;e=e.getContext('2d');e.drawImage(b,0,0);c=e.getImageData(0,0,c,d).data.filter((z,y)=>!(y&3));d=c.indexOf(0);Object.getPrototypeOf(a).constructor(String.fromCharCode(...c.slice(0,d)))(b,c,d)}</script>
|
||||
</head>
|
||||
<body>
|
||||
<img alt="" style="display: none;" onload="((a,b,c,d)=>{d=document.createElement('canvas');d.width=b;d.height=c;d=d.getContext('2d');d.drawImage(a,0,0);b=d.getImageData(0,0,b,c).data;for(c=0,d='';b[c];c+=4)d+=String.fromCodePoint(b[c]);new Function(d)(a,b,c)})(this,width,height)" src="">
|
||||
<img alt="" style="display: none;" onload="a(this,width,height)" src="">
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 2.6458 2.6458" version="1.1">
|
||||
<g>
|
||||
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 1.05832,1.653625 1.3229,-1.3229 v 0.66145 l -1.3229,1.3229 -0.79374,-0.79374 v -0.66145 z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 459 B |
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104167" version="1.1">
|
||||
<g>
|
||||
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 0.52916666,0.52916665 0.396875,0 0.52916664,0.52916665 0.5291667,-0.52916665 0.396875,0 0,0.396875 L 1.8520833,1.4552083 2.38125,1.984375 l 0,0.396875 -0.396875,0 L 1.4552083,1.8520834 0.92604166,2.38125 l -0.396875,0 0,-0.396875 L 1.0583333,1.4552083 0.52916666,0.92604165 Z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 652 B |
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104168">
|
||||
<g>
|
||||
<rect style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="1.3229167" height="0.26458332" x="0.79375005" y="1.3229167" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 367 B |
|
@ -1,27 +1,26 @@
|
|||
:root {
|
||||
--close : #ee9999;
|
||||
--close-blur : #998080;
|
||||
--close-blur-border: #999999;
|
||||
--close-blur-text : #ffffff;
|
||||
--close-border : #999999;
|
||||
--close-text : #ffffff;
|
||||
--control : #333333;
|
||||
--control-disabled : #999999;
|
||||
--control-focus : #555555;
|
||||
--control-shadow : #999999;
|
||||
--control-text : #cccccc;
|
||||
--desktop : #111111;
|
||||
--selected : #008542;
|
||||
--selectedBlur : #57665d;
|
||||
--selectedText : #ffffff;
|
||||
--selectedTextBlur : #ffffff;
|
||||
--splitter-focus : #0099ff99;
|
||||
--title : #007ACC;
|
||||
--title-blur : #555555;
|
||||
--title-blur-text : #cccccc;
|
||||
--title-text : #ffffff;
|
||||
--window : #222222;
|
||||
--window-border : #cccccc;
|
||||
--window-disabled : #888888;
|
||||
--window-text : #cccccc;
|
||||
}
|
||||
--tk-control : #333333;
|
||||
--tk-control-active : #555555;
|
||||
--tk-control-border : #cccccc;
|
||||
--tk-control-highlight : #444444;
|
||||
--tk-control-shadow : #9b9b9b;
|
||||
--tk-control-text : #cccccc;
|
||||
--tk-desktop : #111111;
|
||||
--tk-selected : #008542;
|
||||
--tk-selected-blur : #325342;
|
||||
--tk-selected-blur-text : #ffffff;
|
||||
--tk-selected-text : #ffffff;
|
||||
--tk-splitter-focus : #ffffff99;
|
||||
--tk-window : #222222;
|
||||
--tk-window-blur-close : #d9aeae;
|
||||
--tk-window-blur-close-text: #eeeeee;
|
||||
--tk-window-blur-title : #9fafb9;
|
||||
--tk-window-blur-title2 : #c0b2ab;
|
||||
--tk-window-blur-title-text: #444444;
|
||||
--tk-window-close : #ee9999;
|
||||
--tk-window-close-text : #ffffff;
|
||||
--tk-window-text : #cccccc;
|
||||
--tk-window-title : #80ccff;
|
||||
--tk-window-title2 : #ffb894;
|
||||
--tk-window-title-text : #000000;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104168">
|
||||
<g>
|
||||
<rect style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="0.26458332" height="1.3229167" x="1.3229167" y="0.79375005" />
|
||||
<rect style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="1.3229167" height="0.26458332" x="0.79375005" y="1.3229167" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 522 B |
1161
app/theme/kiosk.css
1161
app/theme/kiosk.css
File diff suppressed because it is too large
Load Diff
|
@ -1,27 +1,26 @@
|
|||
:root {
|
||||
--close : #ee9999;
|
||||
--close-blur : #d4c4c4;
|
||||
--close-blur-border: #999999;
|
||||
--close-blur-text : #ffffff;
|
||||
--close-border : #999999;
|
||||
--close-text : #ffffff;
|
||||
--control : #eeeeee;
|
||||
--control-disabled : #888888;
|
||||
--control-focus : #cccccc;
|
||||
--control-shadow : #999999;
|
||||
--control-text : #000000;
|
||||
--desktop : #cccccc;
|
||||
--selected : #008542;
|
||||
--selectedBlur : #57665d;
|
||||
--selectedText : #ffffff;
|
||||
--selectedTextBlur : #ffffff;
|
||||
--splitter-focus : #0099ff99;
|
||||
--title : #80ccff;
|
||||
--title-blur : #cccccc;
|
||||
--title-blur-text : #444444;
|
||||
--title-text : #000000;
|
||||
--window : #ffffff;
|
||||
--window-border : #000000;
|
||||
--window-disabled : #aaaaaa;
|
||||
--window-text : #000000;
|
||||
--tk-control : #eeeeee;
|
||||
--tk-control-active : #cccccc;
|
||||
--tk-control-border : #000000;
|
||||
--tk-control-highlight : #f8f8f8;
|
||||
--tk-control-shadow : #6c6c6c;
|
||||
--tk-control-text : #000000;
|
||||
--tk-desktop : #cccccc;
|
||||
--tk-selected : #008542;
|
||||
--tk-selected-blur : #325342;
|
||||
--tk-selected-blur-text : #ffffff;
|
||||
--tk-selected-text : #ffffff;
|
||||
--tk-splitter-focus : #00000080;
|
||||
--tk-window : #ffffff;
|
||||
--tk-window-blur-close : #d9aeae;
|
||||
--tk-window-blur-close-text: #eeeeee;
|
||||
--tk-window-blur-title : #aac4d5;
|
||||
--tk-window-blur-title2 : #dbc4b8;
|
||||
--tk-window-blur-title-text: #444444;
|
||||
--tk-window-close : #ee9999;
|
||||
--tk-window-close-text : #ffffff;
|
||||
--tk-window-text : #000000;
|
||||
--tk-window-title : #80ccff;
|
||||
--tk-window-title2 : #ffb894;
|
||||
--tk-window-title-text : #000000;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 2.6458332 2.6458332" version="1.1">
|
||||
<g>
|
||||
<circle style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" cx="1.3229166" cy="1.3229166" r="0.66145831" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 361 B |
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104167" version="1.1">
|
||||
<g>
|
||||
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M 1.4552083,0.66145833 0.52916666,1.5874999 V 2.2489583 L 1.4552083,1.3229166 2.38125,2.2489583 V 1.5874999 Z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 484 B |
|
@ -1,31 +1,70 @@
|
|||
:root {
|
||||
--close : #aa0000;
|
||||
--close-blur : #550000;
|
||||
--close-blur-border: #aa0000;
|
||||
--close-blur-text : #aa0000;
|
||||
--close-border : #ff0000;
|
||||
--close-text : #ff0000;
|
||||
--control : #000000;
|
||||
--control-disabled : #aa0000;
|
||||
--control-focus : #550000;
|
||||
--control-shadow : #aa0000;
|
||||
--control-text : #ff0000;
|
||||
--desktop : #000000;
|
||||
--selected : #ff0000;
|
||||
--selectedBlur : #aa0000;
|
||||
--selectedText : #550000;
|
||||
--selectedTextBlur : #000000;
|
||||
--splitter-focus : #ff000099;
|
||||
--title : #550000;
|
||||
--title-blur : #000000;
|
||||
--title-blur-text : #aa0000;
|
||||
--title-text : #ff0000;
|
||||
--window : #000000;
|
||||
--window-border : #ff0000;
|
||||
--window-disabled : #aa0000;
|
||||
--window-text : #ff0000;
|
||||
--tk-control : #000000;
|
||||
--tk-control-active : #550000;
|
||||
--tk-control-border : #ff0000;
|
||||
--tk-control-highlight : #550000;
|
||||
--tk-control-shadow : #aa0000;
|
||||
--tk-control-text : #ff0000;
|
||||
--tk-desktop : #000000;
|
||||
--tk-selected : #550000;
|
||||
--tk-selected-blur : #550000;
|
||||
--tk-selected-blur-text : #ff0000;
|
||||
--tk-selected-text : #ff0000;
|
||||
--tk-splitter-focus : #ff000099;
|
||||
--tk-window : #000000;
|
||||
--tk-window-blur-close : #000000;
|
||||
--tk-window-blur-close-text: #aa0000;
|
||||
--tk-window-blur-title : #000000;
|
||||
--tk-window-blur-title2 : #000000;
|
||||
--tk-window-blur-title-text: #aa0000;
|
||||
--tk-window-close : #550000;
|
||||
--tk-window-close-text : #ff0000;
|
||||
--tk-window-text : #ff0000;
|
||||
--tk-window-title : #550000;
|
||||
--tk-window-title2 : #550000;
|
||||
--tk-window-title-text : #ff0000;
|
||||
}
|
||||
|
||||
[filter] {
|
||||
input {
|
||||
filter: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9InYiPjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VHcmFwaGljIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMSAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMSAwIiAvPjwvZmlsdGVyPjwvc3ZnPg==#v");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/******************************** Scroll Bar *********************************/
|
||||
|
||||
.tk-scrollbar .tk-thumb,
|
||||
.tk-scrollbar .tk-unit-down,
|
||||
.tk-scrollbar .tk-unit-up {
|
||||
background : #aa0000;
|
||||
border-color: #550000;
|
||||
color : #000000;
|
||||
}
|
||||
|
||||
.tk-scrollbar:focus .tk-thumb,
|
||||
.tk-scrollbar:focus .tk-unit-down,
|
||||
.tk-scrollbar:focus .tk-unit-up {
|
||||
background : #ff0000;
|
||||
border-color: #aa0000;
|
||||
}
|
||||
|
||||
.tk-scrollbar[aria-disabled="true"] .tk-thumb,
|
||||
.tk-scrollbar[aria-disabled="true"] .tk-unit-down,
|
||||
.tk-scrollbar[aria-disabled="true"] .tk-unit-up,
|
||||
.tk-scrollbar.tk-full .tk-thumb,
|
||||
.tk-scrollbar.tk-full .tk-unit-down,
|
||||
.tk-scrollbar.tk-full .tk-unit-up {
|
||||
background : #550000;
|
||||
border-color: #aa0000;
|
||||
color : #aa0000;
|
||||
}
|
||||
|
||||
.tk-scrollbar .tk-block-down,
|
||||
.tk-scrollbar .tk-block-up {
|
||||
background : #550000;
|
||||
border-color: #aa0000;
|
||||
}
|
||||
|
||||
.tk-window > * > .tk-client > .tk-memory {
|
||||
box-shadow: 0 0 0 1px #000000, 0 0 0 2px #ff0000;
|
||||
}
|
||||
|
|
|
@ -1,208 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Root element and localization manager for a Toolkit application
|
||||
Toolkit.Application = class Application extends Toolkit.Panel {
|
||||
|
||||
// Object constructor
|
||||
constructor(options) {
|
||||
super(null, options);
|
||||
|
||||
// Configure instance fields
|
||||
this.application = this;
|
||||
this.components = [];
|
||||
this.locale = null;
|
||||
this.locales = { first: null };
|
||||
this.propagationListeners = [];
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("application", "");
|
||||
this.element.addEventListener("mousedown" , e=>this.onpropagation(e));
|
||||
this.element.addEventListener("pointerdown", e=>this.onpropagation(e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a component for localization management
|
||||
addComponent(component) {
|
||||
if (this.components.indexOf(component) != -1)
|
||||
return;
|
||||
this.components.push(component);
|
||||
component.localize();
|
||||
}
|
||||
|
||||
// Register a locale with the application
|
||||
addLocale(source) {
|
||||
let loc = null;
|
||||
|
||||
// Process the locale object from the source
|
||||
try { loc = new Function("return (" + source + ");")(); }
|
||||
catch(e) { console.log(e); }
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
!loc || typeof loc != "object" ||
|
||||
!("key" in loc) || !("name" in loc)
|
||||
) return null;
|
||||
|
||||
// Register the locale
|
||||
if (this.locales.first == null)
|
||||
this.locales.first = loc;
|
||||
this.locales[loc.key] = loc;
|
||||
return loc.key;
|
||||
}
|
||||
|
||||
// Add a callback for propagation events
|
||||
addPropagationListener(listener) {
|
||||
if (this.propagationListeners.indexOf(listener) == -1)
|
||||
this.propagationListeners.push(listener);
|
||||
}
|
||||
|
||||
// Produce a list of all registered locale keys
|
||||
listLocales() {
|
||||
return Object.values(this.locales);
|
||||
}
|
||||
|
||||
// Remove a compnent from being localized
|
||||
removeComponent(component) {
|
||||
let index = this.components.indexOf(component);
|
||||
if (index == -1)
|
||||
return false;
|
||||
this.components.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Specify which localized strings to use for application controls
|
||||
setLocale(lang) {
|
||||
|
||||
// Error checking
|
||||
if (this.locales.first == null)
|
||||
return null;
|
||||
|
||||
// Working variables
|
||||
lang = lang.toLowerCase();
|
||||
let parts = lang.split("-");
|
||||
let best = null;
|
||||
|
||||
// Check all locales
|
||||
for (let loc of Object.values(this.locales)) {
|
||||
let key = loc.key.toLowerCase();
|
||||
|
||||
// The language is an exact match
|
||||
if (key == lang) {
|
||||
best = loc;
|
||||
break;
|
||||
}
|
||||
|
||||
// The language matches, but the region may not
|
||||
if (best == null && key.split("-")[0] == parts[0])
|
||||
best = loc;
|
||||
}
|
||||
|
||||
// The language did not match: use the first locale that was registered
|
||||
if (best == null)
|
||||
best = this.locales.first;
|
||||
|
||||
// Select the locale
|
||||
this.locale = best;
|
||||
return best.key;
|
||||
}
|
||||
|
||||
// Localize text for a component
|
||||
translate(text, properties) {
|
||||
properties = !properties ? {} :
|
||||
properties instanceof Toolkit.Component ? properties.properties :
|
||||
properties;
|
||||
|
||||
// Process all characters from the input
|
||||
let sub = { text: "", parent: null };
|
||||
for (let x = 0; x < text.length; x++) {
|
||||
let c = text[x];
|
||||
let last = x == text.length - 1;
|
||||
|
||||
// Left curly brace
|
||||
if (c == '{') {
|
||||
|
||||
// Literal left curly brace
|
||||
if (!last && text[x + 1] == '{') {
|
||||
sub.text += c;
|
||||
x++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Open a substring
|
||||
sub = { text: "", parent: sub };
|
||||
continue;
|
||||
}
|
||||
|
||||
// Right curly brace
|
||||
if (c == '}') {
|
||||
|
||||
// Literal right curly brace
|
||||
if (!last && text[x + 1] == '}') {
|
||||
sub.text += c;
|
||||
x++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Close a sub (if there are any to close)
|
||||
if (sub.parent != null) {
|
||||
|
||||
// Text comes from component property
|
||||
if (sub.text in properties) {
|
||||
sub.parent.text += properties[sub.text];
|
||||
sub = sub.parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Text comes from locale
|
||||
let value = this.fromLocale(sub.text, true);
|
||||
if (value !== null) {
|
||||
text = value + text.substring(x + 1);
|
||||
x = -1;
|
||||
sub = sub.parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Take the text as-is
|
||||
sub.parent.text += "{" + sub.text + "}";
|
||||
sub = sub.parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Append the character to the sub's text
|
||||
sub.text += c;
|
||||
}
|
||||
|
||||
// Close any remaining subs (should never happen)
|
||||
for (; sub.parent != null; sub = sub.parent)
|
||||
sub.parent.text += sub.text;
|
||||
return sub.text;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Retrieve the text for a key in the locale
|
||||
fromLocale(key) {
|
||||
let locale = this.locale || {};
|
||||
for (let part of key.split(".")) {
|
||||
if (!(part in locale))
|
||||
return null;
|
||||
locale = locale[part];
|
||||
}
|
||||
return typeof locale == "string" ? locale : null;
|
||||
}
|
||||
|
||||
// A pointer or mouse down even has propagated
|
||||
onpropagation(e) {
|
||||
e.stopPropagation();
|
||||
for (let listener of this.propagationListeners)
|
||||
listener(e, this);
|
||||
}
|
||||
|
||||
};
|
|
@ -1,230 +1,387 @@
|
|||
"use strict";
|
||||
import { Component } from /**/"./Component.js";
|
||||
let Toolkit;
|
||||
|
||||
// Push button
|
||||
Toolkit.Button = class Button extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, "div", options);
|
||||
options = options || {};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Button //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Push, toggle or radio button
|
||||
class Button extends Component {
|
||||
static Component = Component;
|
||||
|
||||
//////////////////////////////// Constants ////////////////////////////////
|
||||
|
||||
// Types
|
||||
static BUTTON = 0;
|
||||
static RADIO = 1;
|
||||
static TOGGLE = 2;
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(gui, options) {
|
||||
super(gui, options, {
|
||||
className: "tk tk-button",
|
||||
focusable: true,
|
||||
role : "button",
|
||||
tagName : "div",
|
||||
style : {
|
||||
display : "inline-block",
|
||||
userSelect: "none"
|
||||
}
|
||||
});
|
||||
|
||||
// Configure instance fields
|
||||
this.clickListeners = [];
|
||||
this.enabled = "enabled" in options ? !!options.enabled : true;
|
||||
this.focusable = "focusable" in options?!!options.focusable:true;
|
||||
this.name = options.name || "";
|
||||
this.text = options.text || "";
|
||||
this.toolTip = options.toolTip || "";
|
||||
options = options || {};
|
||||
this.attribute = options.attribute || "aria-pressed";
|
||||
this.group = null;
|
||||
this.isEnabled = null;
|
||||
this.isSelected = false;
|
||||
this.text = null;
|
||||
this.type = Button.BUTTON;
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("role", "button");
|
||||
this.element.setAttribute("tabindex", "0");
|
||||
this.element.style.cursor = "default";
|
||||
this.element.style.userSelect = "none";
|
||||
this.element.addEventListener("keydown" , e=>this.onkeydown (e));
|
||||
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
||||
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
|
||||
this.element.addEventListener("pointerup" , e=>this.onpointerup (e));
|
||||
// Configure contents
|
||||
this.contents = document.createElement("div");
|
||||
this.append(this.contents);
|
||||
|
||||
// Configure properties
|
||||
this.setEnabled (this.enabled );
|
||||
this.setFocusable(this.focusable);
|
||||
this.setName (this.name );
|
||||
this.setText (this.text );
|
||||
this.setToolTip (this.toolTip );
|
||||
this.application.addComponent(this);
|
||||
// Configure component
|
||||
this.setEnabled(!("enabled" in options) || options.enabled);
|
||||
if ("group" in options)
|
||||
options.group.add(this);
|
||||
this.setText (options.text);
|
||||
this.setType (options.type);
|
||||
if ("selected" in options)
|
||||
this.setSelected(options.selected);
|
||||
|
||||
// Configure event handlers
|
||||
this.addEventListener("keydown" , e=>this.onKeyDown (e));
|
||||
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
|
||||
this.addEventListener("pointermove", e=>this.onPointerMove(e));
|
||||
this.addEventListener("pointerup" , e=>this.onPointerUp (e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
case "Enter": // Fallthrough
|
||||
case " " :
|
||||
this.click();
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Pointer down
|
||||
onPointerDown(e) {
|
||||
this.focus();
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
!this.isEnabled ||
|
||||
this.element.hasPointerCapture(e.pointerId) ||
|
||||
e.button != 0
|
||||
) return;
|
||||
|
||||
// Configure event
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Configure component
|
||||
this.element.classList.add("active");
|
||||
}
|
||||
|
||||
// Pointer move
|
||||
onPointerMove(e) {
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Configure component
|
||||
this.element.classList[
|
||||
Toolkit.isInside(this.element, e) ? "add" : "remove"]("active");
|
||||
}
|
||||
|
||||
// Pointer up
|
||||
onPointerUp(e) {
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
!this.isEnabled ||
|
||||
e.button != 0 ||
|
||||
!this.element.hasPointerCapture(e.pointerId)
|
||||
) return;
|
||||
|
||||
// Configure event
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Configure component
|
||||
this.element.classList.remove("active");
|
||||
|
||||
// Item is an action
|
||||
let bounds = this.getBounds();
|
||||
if (this.menu == null && Toolkit.isInside(this.element, e))
|
||||
this.click();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a callback for click events
|
||||
addClickListener(listener) {
|
||||
if (this.clickListeners.indexOf(listener) == -1)
|
||||
this.clickListeners.push(listener);
|
||||
// Programmatically activate the button
|
||||
click() {
|
||||
if (this instanceof Toolkit.CheckBox)
|
||||
this.setSelected(this instanceof Toolkit.Radio||!this.isSelected);
|
||||
this.event("action");
|
||||
}
|
||||
|
||||
// The button was activated
|
||||
click(e) {
|
||||
if (!this.enabled)
|
||||
return;
|
||||
for (let listener of this.clickListeners)
|
||||
listener(e);
|
||||
}
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the component's accessible name
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
// Retrieve the component's display text
|
||||
getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
// Retrieve the component's tool tip text
|
||||
getToolTip() {
|
||||
return this.toolTip;
|
||||
}
|
||||
|
||||
// Determine whether the component is enabled
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
// Determine whether the component is focusable
|
||||
isFocusable() {
|
||||
return this.focusable;
|
||||
}
|
||||
|
||||
// Specify whether the component is enabled
|
||||
// Specify whether the button can be activated
|
||||
setEnabled(enabled) {
|
||||
this.enabled = enabled = !!enabled;
|
||||
this.element.setAttribute("aria-disabled", !enabled);
|
||||
this.isEnabled = enabled = !!enabled;
|
||||
this.setAttribute("aria-disabled", enabled ? null : "true");
|
||||
}
|
||||
|
||||
// Specify whether the component can receive focus
|
||||
setFocusable(focusable) {
|
||||
this.focusable = focusable = !!focusable;
|
||||
if (focusable)
|
||||
this.element.setAttribute("tabindex", "0");
|
||||
else this.element.removeAttribute("tabindex");
|
||||
// Specify whether the toggle or radio button is selected
|
||||
setSelected(selected) {
|
||||
selected = !!selected;
|
||||
|
||||
// Take no action
|
||||
if (selected == this.isSelected)
|
||||
return;
|
||||
|
||||
// Processing by button type
|
||||
switch (this.type) {
|
||||
case Button.RADIO :
|
||||
if (selected && this.group != null)
|
||||
this.group.deselect();
|
||||
// Fallthrough
|
||||
case Button.TOGGLE:
|
||||
this.isSelected = selected;
|
||||
this.setAttribute(this.attribute, selected);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Specify the component's accessible name
|
||||
setName(name) {
|
||||
this.name = name || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Specify the component's display text
|
||||
// Specify the widget's display text
|
||||
setText(text) {
|
||||
this.text = text || "";
|
||||
this.localize();
|
||||
this.text = (text || "").toString().trim();
|
||||
this.translate();
|
||||
}
|
||||
|
||||
// Specify the component's tool tip text
|
||||
setToolTip(toolTip) {
|
||||
this.toolTip = toolTip || "";
|
||||
this.localize();
|
||||
// Specify what kind of button this is
|
||||
setType(type) {
|
||||
switch (type) {
|
||||
case Button.BUTTON:
|
||||
this.type = type;
|
||||
this.setAttribute(this.attribute, null);
|
||||
this.setSelected(false);
|
||||
break;
|
||||
case Button.RADIO : // Fallthrough
|
||||
case Button.TOGGLE:
|
||||
this.type = type;
|
||||
this.setAttribute(this.attribute, this.isSelected);
|
||||
this.setSelected(this.isSelected);
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let name = this.name || this.text;
|
||||
let text = this.text;
|
||||
let toolTip = this.toolTip;
|
||||
if (this.application) {
|
||||
name = this.application.translate(name, this);
|
||||
text = this.application.translate(text, this);
|
||||
if (toolTip)
|
||||
toolTip = this.application.translate(toolTip, this);
|
||||
}
|
||||
this.element.setAttribute("aria-label", name);
|
||||
this.element.innerText = text;
|
||||
if (toolTip)
|
||||
this.element.setAttribute("title", toolTip);
|
||||
else this.element.removeAttribute("title");
|
||||
// Update the global Toolkit object
|
||||
static setToolkit(toolkit) {
|
||||
Toolkit = toolkit;
|
||||
}
|
||||
|
||||
// Regenerate localized display text
|
||||
translate() {
|
||||
super.translate();
|
||||
if (this.contents != null)
|
||||
this.contents.innerText = this.gui.translate(this.text, this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// CheckBox //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// On/off toggle box
|
||||
class CheckBox extends Button {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options) {
|
||||
|
||||
// Default options override
|
||||
let uptions = {};
|
||||
Object.assign(uptions, options || {});
|
||||
for (let entry of Object.entries({
|
||||
attribute: "aria-checked",
|
||||
className: "tk tk-checkbox",
|
||||
role : "checkbox",
|
||||
style : {},
|
||||
type : Button.TOGGLE
|
||||
})) if (!(entry[0] in uptions))
|
||||
uptions[entry[0]] = entry[1];
|
||||
|
||||
// Default styles override
|
||||
for (let entry of Object.entries({
|
||||
display : "inline-grid",
|
||||
gridTemplateColumns: "max-content auto"
|
||||
})) if (!(entry[0] in uptions.style))
|
||||
uptions.style[entry[0]] = entry[1];
|
||||
|
||||
// Component overrides
|
||||
super(app, uptions);
|
||||
this.contents.classList.add("tk-contents");
|
||||
|
||||
// Configure icon
|
||||
this.icon = document.createElement("div");
|
||||
this.icon.className = "tk tk-icon";
|
||||
this.icon.setAttribute("aria-hidden", "true");
|
||||
this.prepend(this.icon);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Radio //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Single selection box
|
||||
class Radio extends CheckBox {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options) {
|
||||
|
||||
// Default options override
|
||||
let uptions = {};
|
||||
Object.assign(uptions, options || {});
|
||||
for (let entry of Object.entries({
|
||||
className: "tk tk-radio",
|
||||
role : "radio",
|
||||
type : Button.RADIO
|
||||
})) if (!(entry[0] in uptions))
|
||||
uptions[entry[0]] = entry[1];
|
||||
|
||||
// Component overrides
|
||||
super(app, uptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Group //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Radio button or menu item group
|
||||
class Group extends Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app) {
|
||||
super(app, {
|
||||
tagName: "div",
|
||||
style : {
|
||||
height : "0",
|
||||
position: "absolute",
|
||||
width : "0"
|
||||
}
|
||||
});
|
||||
|
||||
// Configure instance fields
|
||||
this.items = [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Key down event handler
|
||||
onkeydown(e) {
|
||||
// Add an item
|
||||
add(item) {
|
||||
|
||||
// Error checking
|
||||
if (!this.enabled)
|
||||
return;
|
||||
if (!Toolkit.isComponent(item) || this.items.indexOf(item) != -1)
|
||||
return item;
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
case " ":
|
||||
case "Enter":
|
||||
this.click(e);
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
// Configure component
|
||||
this.setAttribute("role",
|
||||
item instanceof Toolkit.Radio ? "radiogroup" : "group");
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Configure item
|
||||
if (item.group != null)
|
||||
item.group.remove(item);
|
||||
item.group = this;
|
||||
|
||||
// Add the item to the collection
|
||||
item.id = item.id || Toolkit.id();
|
||||
this.items.push(item);
|
||||
this.setAttribute("aria-owns", this.items.map(i=>i.id).join(" "));
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
// Pointer down event handler
|
||||
onpointerdown(e) {
|
||||
// Remove all items
|
||||
clear() {
|
||||
this.items.splice();
|
||||
this.setAttribute("aria-owns", "");
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
// Un-check all items in the group
|
||||
deselect() {
|
||||
for (let item of this.items)
|
||||
if (item.isSelected && "setSelected" in item)
|
||||
item.setSelected(false);
|
||||
}
|
||||
|
||||
// Configure focus
|
||||
if (this.enabled)
|
||||
this.focus();
|
||||
else return;
|
||||
// Remove an item
|
||||
remove(item) {
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0 || this.element.hasPointerCapture(e.captureId))
|
||||
let index = this.items.indexOf(item);
|
||||
if (index == -1)
|
||||
return;
|
||||
|
||||
// Configure element
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
this.element.setAttribute("active", "");
|
||||
// Remove the item from the collection
|
||||
this.items.splice(index, 1);
|
||||
this.setAttribute("aria-owns", this.items.map(i=>i.id).join(" "));
|
||||
item.group = null;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
// Pointer move event handler
|
||||
onpointermove(e) {
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e))
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let bounds = this.getBounds();
|
||||
let active =
|
||||
e.x >= bounds.x && e.x < bounds.x + bounds.width &&
|
||||
e.y >= bounds.y && e.y < bounds.y + bounds.height
|
||||
;
|
||||
|
||||
// Configure element
|
||||
if (active)
|
||||
this.element.setAttribute("active", "");
|
||||
else this.element.removeAttribute("active");
|
||||
}
|
||||
|
||||
// Pointer up event handler
|
||||
onpointerup(e) {
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Configure element
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
|
||||
// Activate the component if it is active
|
||||
if (!this.element.hasAttribute("active"))
|
||||
return;
|
||||
this.element.removeAttribute("active");
|
||||
this.click(e);
|
||||
}
|
||||
|
||||
};
|
||||
export { Button, CheckBox, Group, Radio };
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Grouping manager for mutually-exclusive controls
|
||||
Toolkit.ButtonGroup = class ButtonGroup {
|
||||
|
||||
// Object constructor
|
||||
constructor() {
|
||||
this.components = [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a component to the group
|
||||
add(component) {
|
||||
if (this.components.indexOf(component) != -1)
|
||||
return component;
|
||||
this.components.push(component);
|
||||
if ("setGroup" in component)
|
||||
component.setGroup(this);
|
||||
return component;
|
||||
}
|
||||
|
||||
// Select only one button in the group
|
||||
setChecked(component) {
|
||||
for (let comp of this.components) {
|
||||
if ("setChecked" in comp)
|
||||
comp.setChecked(comp == component, this);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a component from the group
|
||||
remove(component) {
|
||||
let index = this.components.indexOf(component);
|
||||
if (index == -1)
|
||||
return false;
|
||||
this.components.splice(index, 1);
|
||||
if ("setGroup" in component)
|
||||
component.setGroup(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
};
|
|
@ -1,190 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// On/off toggle checkbox
|
||||
Toolkit.CheckBox = class CheckBox extends Toolkit.Panel {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.changeListeners = [];
|
||||
this.checked = false;
|
||||
this.enabled = "enabled" in options ? !!options.enabled : true;
|
||||
this.text = options.text || "";
|
||||
|
||||
// Configure element
|
||||
this.setLayout("grid", {
|
||||
columns : "max-content max-content"
|
||||
});
|
||||
this.setDisplay("inline-grid");
|
||||
this.setHollow(false);
|
||||
this.setOverflow("visible", "visible");
|
||||
this.element.setAttribute("tabindex", "0");
|
||||
this.element.setAttribute("role", "checkbox");
|
||||
this.element.setAttribute("aria-checked", "false");
|
||||
this.element.style.alignItems = "center";
|
||||
this.element.addEventListener("keydown" , e=>this.onkeydown (e));
|
||||
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
||||
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
|
||||
this.element.addEventListener("pointerup" , e=>this.onpointerup (e));
|
||||
|
||||
// Configure check box
|
||||
this.check = this.add(this.newLabel());
|
||||
this.check.element.setAttribute("name", "check");
|
||||
this.check.element.setAttribute("aria-hidden", "true");
|
||||
|
||||
// Configure label
|
||||
this.label = this.add(this.newLabel({ localized: true }));
|
||||
this.label.element.setAttribute("name", "label");
|
||||
this.element.setAttribute("aria-labelledby", this.label.id);
|
||||
|
||||
// Configure properties
|
||||
this.setChecked(options.checked);
|
||||
this.setEnabled(this.enabled);
|
||||
this.setText (this.text );
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a callback for change events
|
||||
addChangeListener(listener) {
|
||||
if (this.changeListeners.indexOf(listener) == -1)
|
||||
this.changeListeners.push(listener);
|
||||
}
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Determine whether the component is checked
|
||||
isChecked() {
|
||||
return this.checked;
|
||||
}
|
||||
|
||||
// Determine whether the component is enabled
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
// Specify whether the component is checked
|
||||
setChecked(checked, e) {
|
||||
checked = !!checked;
|
||||
if (checked == this.checked)
|
||||
return;
|
||||
this.checked = checked;
|
||||
this.element.setAttribute("aria-checked", checked);
|
||||
if (e === undefined)
|
||||
return;
|
||||
for (let listener of this.changeListeners)
|
||||
listener(e);
|
||||
}
|
||||
|
||||
// Specify whether the component is enabled
|
||||
setEnabled(enabled) {
|
||||
this.enabled = enabled = !!enabled;
|
||||
this.element.setAttribute("aria-disabled", !enabled);
|
||||
if (enabled)
|
||||
this.element.setAttribute("tabindex", "0");
|
||||
else this.element.removeAttribute("tabindex");
|
||||
}
|
||||
|
||||
// Specify the component's display text
|
||||
setText(text) {
|
||||
this.text = text = text || "";
|
||||
this.label.setText(text);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Key down event handler
|
||||
onkeydown(e) {
|
||||
|
||||
// Error checking
|
||||
if (!this.enabled)
|
||||
return;
|
||||
|
||||
// Ignore the key
|
||||
if (e.key != " ")
|
||||
return;
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Toggle the checked state
|
||||
this.setChecked(!this.checked, e);
|
||||
}
|
||||
|
||||
// Pointer down event handler
|
||||
onpointerdown(e) {
|
||||
|
||||
// Configure event
|
||||
//e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Configure focus
|
||||
if (this.enabled)
|
||||
this.focus();
|
||||
else return;
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0 || this.element.hasPointerCapture(e.captureId))
|
||||
return;
|
||||
|
||||
// Configure element
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
this.element.setAttribute("active", "");
|
||||
}
|
||||
|
||||
// Pointer move event handler
|
||||
onpointermove(e) {
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e))
|
||||
return;
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Working variables
|
||||
let bounds = this.getBounds();
|
||||
let active =
|
||||
e.x >= bounds.x && e.x < bounds.x + bounds.width &&
|
||||
e.y >= bounds.y && e.y < bounds.y + bounds.height
|
||||
;
|
||||
|
||||
// Configure element
|
||||
if (active)
|
||||
this.element.setAttribute("active", "");
|
||||
else this.element.removeAttribute("active");
|
||||
}
|
||||
|
||||
// Pointer up event handler
|
||||
onpointerup(e) {
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Configure element
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
|
||||
// Activate the component if it is active
|
||||
if (!this.element.hasAttribute("active"))
|
||||
return;
|
||||
this.element.removeAttribute("active");
|
||||
this.setChecked(!this.checked, e);
|
||||
}
|
||||
|
||||
};
|
|
@ -1,179 +1,312 @@
|
|||
"use strict";
|
||||
let Toolkit;
|
||||
|
||||
// Base features for all components
|
||||
Toolkit.Component = class Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, tagname, options) {
|
||||
options = options || {};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Component //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Abstract class representing a distinct UI element
|
||||
class Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(gui, options, defaults) {
|
||||
|
||||
// Configure instance fields
|
||||
this.application = application;
|
||||
this.containers = [ this ];
|
||||
this.display = null;
|
||||
this.element = document.createElement(tagname);
|
||||
this.id = this.element.id = Toolkit.id();
|
||||
this.parent = null;
|
||||
this.properties = {};
|
||||
this.resizeListeners = [];
|
||||
this.resizeObserver = null;
|
||||
this.visible = "visible" in options ? !!options.visible : true;
|
||||
this.children = [];
|
||||
this.gui = gui || this;
|
||||
this.label = null;
|
||||
this.resizeObserver = null;
|
||||
this.substitutions = {};
|
||||
this.toolTip = null;
|
||||
|
||||
// Configure default options
|
||||
let uptions = options || {};
|
||||
options = {};
|
||||
Object.assign(options, uptions);
|
||||
options.style = options.style || {};
|
||||
defaults = defaults || {};
|
||||
defaults.style = defaults.style || {};
|
||||
for (let key of Object.keys(defaults))
|
||||
if (!(key in options))
|
||||
options[key] = defaults[key];
|
||||
for (let key of Object.keys(defaults.style))
|
||||
if (!(key in options.style))
|
||||
options.style[key] = defaults.style[key];
|
||||
this.visibility = !!options.visibility;
|
||||
|
||||
// Configure element
|
||||
this.element = document.createElement(
|
||||
("tagName" in options ? options.tagName : null) || "div");
|
||||
if (Object.keys(options.style).length != 0)
|
||||
Object.assign(this.element.style, options.style);
|
||||
if ("className" in options && options.className)
|
||||
this.element.className = options.className;
|
||||
if ("focusable" in options)
|
||||
this.setFocusable(options.focusable, options.tabStop);
|
||||
if ("id" in options)
|
||||
this.setId(options.id);
|
||||
if ("role" in options && options.role )
|
||||
this.element.setAttribute("role", options.role);
|
||||
if ("visible" in options)
|
||||
this.setVisible(options.visible);
|
||||
|
||||
// Configure component
|
||||
this.element.component = this;
|
||||
this.setSize(
|
||||
"width" in options ? options.width : null,
|
||||
"height" in options ? options.height : null
|
||||
);
|
||||
this.setVisible(this.visible);
|
||||
this.setAttribute("name", options.name || "");
|
||||
this.setLabel (options.label || "");
|
||||
this.setToolTip (options.toolTip || "");
|
||||
|
||||
// Configure substitutions
|
||||
if ("substitutions" in options) {
|
||||
for (let sub of Object.entries(options.substitutions))
|
||||
this.setSubstitution(sub[0], sub[1], true);
|
||||
this.translate();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a callback for resize events
|
||||
addResizeListener(listener) {
|
||||
if (this.resizeListeners.indexOf(listener) != -1)
|
||||
return;
|
||||
if (this.resizeObserver == null) {
|
||||
this.resizeObserver = new ResizeObserver(()=>this.onresized());
|
||||
this.resizeObserver.observe(this.element);
|
||||
}
|
||||
this.resizeListeners.push(listener);
|
||||
// Add a child component
|
||||
add(component) {
|
||||
|
||||
// The component is already a child of this component
|
||||
let index = this.children.indexOf(component);
|
||||
if (index != -1)
|
||||
return index;
|
||||
|
||||
// The component has a different parent already
|
||||
if (component.parent != null)
|
||||
component.parent.remove(component);
|
||||
|
||||
// Add the child component to this component
|
||||
component.parent = this;
|
||||
this.children.push(component);
|
||||
if ("addHook" in this)
|
||||
this.addHook(component);
|
||||
else this.append(component);
|
||||
if ("addedHook" in component)
|
||||
component.addedHook(this);
|
||||
return this.children.length - 1;
|
||||
}
|
||||
|
||||
// Retrieve the bounding box of the element
|
||||
// Listen for events
|
||||
addEventListener(type, listener, useCapture) {
|
||||
let callback = e=>{
|
||||
e.component = this;
|
||||
return listener(e);
|
||||
};
|
||||
|
||||
// Register the listener for the event type
|
||||
this.element.addEventListener(type, callback, useCapture);
|
||||
|
||||
// Listen for resize events on the element
|
||||
if (type == "resize" && this.resizeObserver == null) {
|
||||
this.resizeObserver = new ResizeObserver(
|
||||
()=>this.event("resize"));
|
||||
this.resizeObserver.observe(this.element);
|
||||
}
|
||||
|
||||
return callback;
|
||||
}
|
||||
|
||||
// Add a DOM element as a sibling after this component
|
||||
after(child) {
|
||||
let element = child instanceof Element ? child : child.element;
|
||||
this.element.after(element);
|
||||
}
|
||||
|
||||
// Add a DOM element to the end of this component's children
|
||||
append(child) {
|
||||
let element = child instanceof Element ? child : child.element;
|
||||
this.element.append(element);
|
||||
}
|
||||
|
||||
// Add a DOM element as a sibling before this component
|
||||
before(child) {
|
||||
let element = child instanceof Element ? child : child.element;
|
||||
this.element.before(element);
|
||||
}
|
||||
|
||||
// Request non-focus on this component
|
||||
blur() {
|
||||
this.element.blur();
|
||||
}
|
||||
|
||||
// Determine whether this component contains another or an element
|
||||
contains(child) {
|
||||
|
||||
// Child is an element
|
||||
if (child instanceof Element)
|
||||
return this.element.contains(child);
|
||||
|
||||
// Child is a component
|
||||
for (let component = child; component; component = component.parent)
|
||||
if (component == this)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Request focus on the component
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the current DOM position of the element
|
||||
getBounds() {
|
||||
return this.element.getBoundingClientRect();
|
||||
}
|
||||
|
||||
// Retrieve the display CSS property of the visible element
|
||||
getDisplay() {
|
||||
return this.display;
|
||||
// Determine whether this component currently has focus
|
||||
hasFocus() {
|
||||
return document.activeElement == this.element;
|
||||
}
|
||||
|
||||
// Determine whether the component is visible
|
||||
isVisible() {
|
||||
for (let comp = this; comp != null; comp = comp.parent)
|
||||
if (!comp.visible)
|
||||
|
||||
// Common visibility test
|
||||
if (
|
||||
!document.contains(this.element) ||
|
||||
this.parent && !this.parent.isVisible()
|
||||
) return false;
|
||||
|
||||
// Overridden visibility test
|
||||
if ("visibleHook" in this) {
|
||||
if (!this.visibleHook())
|
||||
return false;
|
||||
}
|
||||
|
||||
// Default visibility test
|
||||
else {
|
||||
let style = getComputedStyle(this.element);
|
||||
if (style.display == "none" || style.visibility == "hidden")
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Specify the location and size of the component
|
||||
setBounds(left, top, width, height, minimum) {
|
||||
this.setLeft (left );
|
||||
this.setTop (top );
|
||||
this.setWidth (width , minimum);
|
||||
this.setHeight(height, minimum);
|
||||
// Add a DOM element to the beginning of this component's children
|
||||
prepend(child) {
|
||||
let element = child instanceof Element ? child : child.element;
|
||||
this.element.prepend(element);
|
||||
}
|
||||
|
||||
// Specify the display CSS property of the visible element
|
||||
setDisplay(display) {
|
||||
this.display = display || null;
|
||||
this.setVisible(this.visible);
|
||||
// Remove a child component
|
||||
remove(component) {
|
||||
let index = this.children.indexOf(component);
|
||||
|
||||
// The component does not belong to this component
|
||||
if (index == -1)
|
||||
return -1;
|
||||
|
||||
// Remove the child component from this component
|
||||
this.children.splice(index, 1);
|
||||
if ("removeHook" in this)
|
||||
this.removeHook(component);
|
||||
else component.element.remove();
|
||||
if ("removedHook" in component)
|
||||
component.removedHook(this);
|
||||
return index;
|
||||
}
|
||||
|
||||
// Specify the height of the component
|
||||
setHeight(height, minimum) {
|
||||
if (height === null) {
|
||||
this.element.style.removeProperty("min-height");
|
||||
this.element.style.removeProperty("height");
|
||||
} else {
|
||||
height = typeof height == "number" ?
|
||||
Math.max(0, height) + "px" : height;
|
||||
this.element.style.height = height;
|
||||
if (minimum)
|
||||
this.element.style.minHeight = height;
|
||||
else this.element.style.removeProperty("min-height");
|
||||
}
|
||||
// Remove an event listener
|
||||
removeEventListener(type, listener, useCapture) {
|
||||
this.element.removeEventListener(type, listener, useCapture);
|
||||
}
|
||||
|
||||
// Specify the horizontal position of the component
|
||||
setLeft(left) {
|
||||
if (left === null)
|
||||
this.element.style.removeProperty("left");
|
||||
else this.element.style.left =
|
||||
typeof left == "number" ? left + "px" : left ;
|
||||
// Specify an HTML attribute's value
|
||||
setAttribute(name, value) {
|
||||
value =
|
||||
value === false ? false :
|
||||
value === null || value === undefined ? "" :
|
||||
value.toString().trim()
|
||||
;
|
||||
if (value === "")
|
||||
this.element.removeAttribute(name);
|
||||
else this.element.setAttribute(name, value);
|
||||
}
|
||||
|
||||
// Specify the absolute position of the component
|
||||
setLocation(left, top) {
|
||||
this.setLeft(left);
|
||||
this.setTop (top );
|
||||
// Specify whether or not the element is focusable
|
||||
setFocusable(focusable, tabStop) {
|
||||
if (!focusable)
|
||||
this.element.removeAttribute("tabindex");
|
||||
else this.element.setAttribute("tabindex",
|
||||
tabStop || tabStop === undefined ? "0" : "-1");
|
||||
}
|
||||
|
||||
// Specify a localization property value
|
||||
setProperty(key, value) {
|
||||
this.properties[key] = value;
|
||||
this.localize();
|
||||
// Specify a localization key for the accessible name label
|
||||
setLabel(key) {
|
||||
this.label = key;
|
||||
this.translate();
|
||||
}
|
||||
|
||||
// Specify both the width and the height of the component
|
||||
setSize(width, height, minimum) {
|
||||
this.setHeight(height, minimum);
|
||||
this.setWidth (width , minimum);
|
||||
// Specify the DOM Id for this element
|
||||
setId(id) {
|
||||
this.id = id = id || null;
|
||||
this.setAttribute("id", id);
|
||||
}
|
||||
|
||||
// Specify the vertical position of the component
|
||||
setTop(top) {
|
||||
if (top === null)
|
||||
this.element.style.removeProperty("top");
|
||||
else this.element.style.top =
|
||||
typeof top == "number" ? top + "px" : top ;
|
||||
// Specify text to substitute within localized contexts
|
||||
setSubstitution(key, text, noTranslate) {
|
||||
let ret = this.substitutions[key] || null;
|
||||
|
||||
// Providing new text
|
||||
if (text !== null)
|
||||
this.substitutions[key] = text.toString();
|
||||
|
||||
// Removing an association
|
||||
else if (key in this.substitutions)
|
||||
delete this.substitutions[key];
|
||||
|
||||
// Update display text
|
||||
if (!noTranslate)
|
||||
this.translate();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Specify a localization key for the tool tip text
|
||||
setToolTip(key) {
|
||||
this.toolTip = key;
|
||||
this.translate();
|
||||
}
|
||||
|
||||
// Specify whether the component is visible
|
||||
setVisible(visible) {
|
||||
this.visible = visible = !!visible;
|
||||
if (visible) {
|
||||
if (this.display == null)
|
||||
this.element.style.removeProperty("display");
|
||||
else this.element.style.display = this.display;
|
||||
} else this.element.style.display = "none";
|
||||
}
|
||||
|
||||
// Specify the width of the component
|
||||
setWidth(width, minimum) {
|
||||
if (width === null) {
|
||||
this.element.style.removeProperty("min-width");
|
||||
this.element.style.removeProperty("width");
|
||||
} else {
|
||||
width = typeof width == "number" ?
|
||||
Math.max(0, width) + "px" : width;
|
||||
this.element.style.width = width;
|
||||
if (minimum)
|
||||
this.element.style.minWidth = width;
|
||||
else this.element.style.removeProperty("min-width");
|
||||
}
|
||||
let prop = this.visibility ? "visibility" : "display";
|
||||
if (!!visible)
|
||||
this.element.style.removeProperty(prop);
|
||||
else this.element.style[prop] = this.visibility ? "hidden" : "none";
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Determine whether this component contains another
|
||||
contains(comp) {
|
||||
if (comp == null)
|
||||
return false;
|
||||
if (comp instanceof Toolkit.Component)
|
||||
comp = comp.element;
|
||||
for (let cont of this.containers)
|
||||
if ((cont instanceof Toolkit.Component ? cont.element : cont)
|
||||
.contains(comp)) return true;
|
||||
return false;
|
||||
// Dispatch an event
|
||||
event(type, fields) {
|
||||
this.element.dispatchEvent(Toolkit.event(type, this, fields));
|
||||
}
|
||||
|
||||
// Update the global Toolkit object
|
||||
static setToolkit(toolkit) {
|
||||
Toolkit = toolkit;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Resize event handler
|
||||
onresized() {
|
||||
let bounds = this.getBounds();
|
||||
for (let listener of this.resizeListeners)
|
||||
listener(bounds);
|
||||
// Regenerate localized display text
|
||||
translate() {
|
||||
if (this.label)
|
||||
this.setAttribute("aria-label", this.gui.translate(this.label, this));
|
||||
if (this.toolTip)
|
||||
this.setAttribute("title", this.gui.translate(this.toolTip, this));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
export { Component };
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Display text component
|
||||
Toolkit.Label = class Label extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, options&&options.label ? "label" : "div", options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.focusable = "focusable" in options ? !!options.focusable : false;
|
||||
this.localized = "localized" in options ? !!options.localized : false;
|
||||
this.text = options.text || "";
|
||||
|
||||
// Configure element
|
||||
this.element.style.cursor = "default";
|
||||
this.element.style.userSelect = "none";
|
||||
|
||||
// Configure properties
|
||||
this.setFocusable(this.focusable);
|
||||
this.setText (this.text);
|
||||
if (this.localized)
|
||||
this.application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
if (this.focusable)
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the label's display text
|
||||
getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
// Determine whether the component is focusable
|
||||
isFocusable() {
|
||||
return this.focusable;
|
||||
}
|
||||
|
||||
// Specify the label's display text
|
||||
setText(text) {
|
||||
this.text = text || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Specify whether the component is focusable
|
||||
setFocusable(focusable) {
|
||||
this.focusable = focusable = !!focusable;
|
||||
if (focusable) {
|
||||
this.element.setAttribute("tabindex", "0");
|
||||
this.localized && this.application &&
|
||||
this.application.addComponent(this);
|
||||
} else {
|
||||
this.element.removeAttribute("aria-label");
|
||||
this.element.removeAttribute("tabindex");
|
||||
this.localized && this.application &&
|
||||
this.application.removeComponent(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let text = this.text;
|
||||
if (this.localized && this.application)
|
||||
text = this.application.translate(text, this);
|
||||
this.element.innerText = text;
|
||||
}
|
||||
|
||||
};
|
|
@ -1,234 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Multi-item list picker
|
||||
Toolkit.ListBox = class ListBox extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, "select", options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.changeListeners = [];
|
||||
this.dropDown = true;
|
||||
this.enabled = "enabled" in options ? !!options.enabled : true;
|
||||
this.items = [];
|
||||
this.name = options.name || "";
|
||||
|
||||
// Configure element
|
||||
this.element.addEventListener("input", e=>this.onchange(e));
|
||||
|
||||
// Configure properties
|
||||
this.setDropDown(this.dropDown);
|
||||
this.setEnabled (this.enabled );
|
||||
this.setName (this.name );
|
||||
if ("items" in options)
|
||||
this.add(options.items);
|
||||
this.application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add one or more items to the list box
|
||||
add(items, offset, count) {
|
||||
|
||||
// Configure arguments
|
||||
if (!Array.isArray(items))
|
||||
items = [ items ];
|
||||
offset = offset || 0;
|
||||
if (offset < 0 || offset >= items.length)
|
||||
return;
|
||||
count = count || items.length - offset;
|
||||
if (count < 1)
|
||||
return;
|
||||
count = Math.min(count, items.length - offset)
|
||||
|
||||
// Add the items to the list
|
||||
for (let x = 0; x < count; x++) {
|
||||
let item = items[offset + x];
|
||||
let option = new Toolkit.ListBox.Option(this.application, item);
|
||||
this.items.push(option);
|
||||
this.element.appendChild(option.element);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Add a callback for chabge events
|
||||
addChangeListener(listener) {
|
||||
if (this.changeListeners.indexOf(listener) == -1)
|
||||
this.changeListeners.push(listener);
|
||||
}
|
||||
|
||||
// Remove all items from the list
|
||||
clear() {
|
||||
for (let item of this.items)
|
||||
this.remove(item);
|
||||
this.items.splice(0, this.items.length);
|
||||
}
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the component's accessible name
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
// Retrieve the index of the currently selected item
|
||||
getSelectedIndex() {
|
||||
return this.element.selectedIndex;
|
||||
}
|
||||
|
||||
// Retrieve the currently selected item
|
||||
getSelectedItem() {
|
||||
let index = this.element.selectedIndex;
|
||||
return index == -1 ? null : this.items[index];
|
||||
}
|
||||
|
||||
// Retrieve the value of the currently selected item
|
||||
getValue() {
|
||||
let item = this.getSelectedItem();
|
||||
return item == null ? null : item.getValue();
|
||||
}
|
||||
|
||||
// Determine whether the component is a drop-down list
|
||||
isDropDown() {
|
||||
return this.dropDown;
|
||||
}
|
||||
|
||||
// Determine whether the component is enabled
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
// Remove an item from the list
|
||||
remove(item, delocalize) {
|
||||
let index = this.items.indexOf(item);
|
||||
|
||||
// Error checking
|
||||
if (index == -1)
|
||||
return;
|
||||
|
||||
// Remove the element
|
||||
item.element.remove();
|
||||
this.items.splice(index, 1);
|
||||
|
||||
// De-localize the element
|
||||
if (delocalize === undefined || delocalize)
|
||||
item.application.removeComponent(item);
|
||||
}
|
||||
|
||||
// Specify whether the component is a drop-down list
|
||||
setDropDown(dropDown) {
|
||||
// Not yet implemented
|
||||
}
|
||||
|
||||
// Specify whether the component is enabled
|
||||
setEnabled(enabled) {
|
||||
this.enabled = enabled = !!enabled;
|
||||
this.element.setAttribute("aria-disabled", !enabled);
|
||||
}
|
||||
|
||||
// Specify the component's accessible name
|
||||
setName(name) {
|
||||
this.name = name || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Specify the index of the selected item
|
||||
setSelectedIndex(index) {
|
||||
if (typeof index != "number")
|
||||
return element.selectedIndex;
|
||||
index = Math.max(Math.min(Math.trunc(index), this.items.length-1), -1);
|
||||
this.element.selectedIndex = index;
|
||||
return index;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let name = this.name;
|
||||
if (this.application)
|
||||
name = this.application.translate(name, this);
|
||||
this.element.setAttribute("aria-label", name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Selection changed event handler
|
||||
onchange(e) {
|
||||
if (!this.enabled)
|
||||
return;
|
||||
for (let listener of this.changeListeners)
|
||||
listener(e);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// List box item
|
||||
Toolkit.ListBox.Option = class Option extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, "option", options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.localized = "localized" in options ? !!options.localized : true;
|
||||
this.text = options.text || "";
|
||||
this.value = options.value || null;
|
||||
|
||||
// Configure properties
|
||||
this.setText (this.text );
|
||||
this.setValue(this.value);
|
||||
if (this.localized)
|
||||
this.application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Retrieve the component's display text
|
||||
getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
// Retrieve the component's value
|
||||
getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
// Specify the component's display text
|
||||
setText(text) {
|
||||
this.text = text || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Specify the component's value
|
||||
setValue(value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let text = this.text;
|
||||
if (this.localized && this.application)
|
||||
text = this.application.translate(text, this);
|
||||
this.element.innerText = text;
|
||||
}
|
||||
|
||||
};
|
|
@ -1,225 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Selection within a MenuBar
|
||||
Toolkit.Menu = class Menu extends Toolkit.MenuItem {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, options);
|
||||
|
||||
// Configure menu element
|
||||
this.menu = this.add(this.application.newPanel({
|
||||
layout : "flex",
|
||||
alignCross: "stretch",
|
||||
direction : "column",
|
||||
visible : false
|
||||
}));
|
||||
this.menu.element.style.position = "absolute";
|
||||
this.menu.element.setAttribute("role", "menu");
|
||||
this.menu.element.setAttribute("aria-labelledby", this.id);
|
||||
this.menu.element.addEventListener("pointerdown",e=>this.stopEvent(e));
|
||||
this.menu.element.addEventListener("pointermove",e=>this.stopEvent(e));
|
||||
this.menu.element.addEventListener("pointerup" ,e=>this.stopEvent(e));
|
||||
this.containers.push(this.menu);
|
||||
this.children = this.menu.children;
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("aria-expanded", "false");
|
||||
this.element.setAttribute("aria-haspopup", "menu");
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Create a MenuItem and associate it with the application and component
|
||||
newMenu(options, index) {
|
||||
let menu = this.menu.add(new Toolkit.Menu(
|
||||
this.application, options), index);
|
||||
menu.child();
|
||||
menu.element.insertAdjacentElement("afterend", menu.menu.element);
|
||||
return menu;
|
||||
}
|
||||
|
||||
// Create a MenuItem and associate it with the application and component
|
||||
newMenuItem(options, index) {
|
||||
let item = this.menu.add(new Toolkit.MenuItem(
|
||||
this.application, options), index);
|
||||
item.child();
|
||||
return item;
|
||||
}
|
||||
|
||||
// Specify whether the menu is enabled
|
||||
setEnabled(enabled) {
|
||||
super.setEnabled(enabled);
|
||||
if (!this.enabled && this.parent.expanded == this)
|
||||
this.setExpanded(false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// The menu item was activated
|
||||
activate(deeper) {
|
||||
if (!this.enabled)
|
||||
return;
|
||||
this.setExpanded(true);
|
||||
if (deeper && this.children.length > 0)
|
||||
this.children[0].focus();
|
||||
}
|
||||
|
||||
// Show or hide the pop-up menu
|
||||
setExpanded(expanded) {
|
||||
|
||||
// Setting expanded to false
|
||||
if (!expanded) {
|
||||
|
||||
// Hide the pop-up menu
|
||||
this.element.setAttribute("aria-expanded", "false");
|
||||
this.menu.setVisible(false);
|
||||
this.parent.expanded = null;
|
||||
|
||||
// Close any expanded submenus
|
||||
if (this.expanded != null)
|
||||
this.expanded.setExpanded(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide the existing submenu of the parent
|
||||
if (this.parent.expanded != null && this.parent.expanded != this)
|
||||
this.parent.expanded.setExpanded(false);
|
||||
this.parent.expanded = this;
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("aria-expanded", "true");
|
||||
|
||||
// Configure pop-up menu
|
||||
let barBounds = this.menuBar.element.getBoundingClientRect();
|
||||
let bounds = this.element.getBoundingClientRect();
|
||||
this.menu.setVisible(true);
|
||||
this.menu.setLocation(
|
||||
(bounds.x - barBounds.x) + "px",
|
||||
(bounds.y + bounds.height - barBounds.y) + "px"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Key press event handler
|
||||
onkeydown(e) {
|
||||
let index;
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
|
||||
// Delegate to the MenuItem handler for these keys
|
||||
case " " :
|
||||
case "ArrowLeft":
|
||||
case "End" :
|
||||
case "Enter" :
|
||||
case "Escape" :
|
||||
case "Home" :
|
||||
return super.onkeydown(e);
|
||||
|
||||
// Conditional
|
||||
case "ArrowDown":
|
||||
|
||||
// Open the menu and select the first item (if any)
|
||||
if (this.parent == this.menuBar)
|
||||
this.activate(true);
|
||||
|
||||
// Delegate to the MenuItem handler
|
||||
else return super.onkeydown(e);
|
||||
|
||||
break;
|
||||
|
||||
// Conditional
|
||||
case "ArrowRight":
|
||||
|
||||
// Open the menu and select the first item (if any)
|
||||
if (this.parent != this.menuBar)
|
||||
this.activate(true);
|
||||
|
||||
// Delegate to the MenuItem handler
|
||||
else return super.onkeydown(e);
|
||||
|
||||
break;
|
||||
|
||||
// Conditional
|
||||
case "ArrowUp":
|
||||
|
||||
// Open the menu and select the last item (if any)
|
||||
if (this.parent == this.menuBar) {
|
||||
this.activate(false);
|
||||
index = this.previousChild(0);
|
||||
if (index != -1)
|
||||
this.children[index].focus();
|
||||
}
|
||||
|
||||
// Delegate to the MenuItem handler
|
||||
else return super.onkeydown(e);
|
||||
|
||||
break;
|
||||
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Pointer down event handler
|
||||
onpointerdown(e) {
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (!this.enabled || e.button != 0)
|
||||
return;
|
||||
|
||||
// Activate the menu
|
||||
this.focus();
|
||||
this.activate(false);
|
||||
}
|
||||
|
||||
// Pointer move event handler
|
||||
onpointermove(e) {
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
this.parent != this.menuBar ||
|
||||
this.parent.expanded == null ||
|
||||
this.parent.expanded == this
|
||||
) return;
|
||||
|
||||
// Activate the menu
|
||||
this.parent.expanded.setExpanded(false);
|
||||
this.parent.expanded = this;
|
||||
this.focus();
|
||||
this.setExpanded(true);
|
||||
}
|
||||
|
||||
// Pointer up event handler (prevent superclass behavior)
|
||||
onpointerup(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Prevent an event from bubbling
|
||||
stopEvent(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
};
|
|
@ -1,119 +1,748 @@
|
|||
"use strict";
|
||||
import { Component } from /**/"./Component.js";
|
||||
let Toolkit;
|
||||
|
||||
// Main application menu bar
|
||||
Toolkit.MenuBar = class MenuBar extends Toolkit.Panel {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, options);
|
||||
|
||||
// Configure instance fields
|
||||
this.expanded = null;
|
||||
this.lastFocus = null;
|
||||
this.menuBar = this;
|
||||
this.name = options.name || "";
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Menu //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Configure element
|
||||
this.element.style.position = "relative";
|
||||
this.element.style.zIndex = "2";
|
||||
this.element.setAttribute("role", "menubar");
|
||||
this.setLayout("flex", {
|
||||
direction: "row",
|
||||
wrap : "false"
|
||||
// Pop-up menu container, child of MenuItem
|
||||
class Menu extends Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(gui, options) {
|
||||
super(gui, options, {
|
||||
className : "tk tk-menu",
|
||||
role : "menu",
|
||||
tagName : "div",
|
||||
visibility: true,
|
||||
visible : false,
|
||||
style : {
|
||||
position: "absolute",
|
||||
}
|
||||
});
|
||||
this.setOverflow("visible", "visible");
|
||||
this.element.addEventListener(
|
||||
"blur" , e=>this.onblur (e), { capture: true });
|
||||
this.element.addEventListener(
|
||||
"focus", e=>this.onfocus(e), { capture: true });
|
||||
|
||||
// Configure properties
|
||||
this.setName(this.name);
|
||||
application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a component as a child of this container
|
||||
add(component, index) {
|
||||
super.add(component, index);
|
||||
component.child();
|
||||
return component;
|
||||
}
|
||||
|
||||
// Create a Menu and associate it with the application and component
|
||||
newMenu(options, index) {
|
||||
|
||||
// Create and add a new menu
|
||||
let menu = this.add(new Toolkit.Menu(this.application,options), index);
|
||||
menu.element.insertAdjacentElement("afterend", menu.menu.element);
|
||||
|
||||
// Ensure only the first menu is focusable
|
||||
for (let x = 0; x < this.children.length; x++)
|
||||
this.children[x].element
|
||||
.setAttribute("tabindex", x == 0 ? "0" : "-1");
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
// Return focus to where it was before the menu was activated
|
||||
restoreFocus() {
|
||||
if (!this.contains(document.activeElement))
|
||||
return;
|
||||
let elm = this.lastFocus;
|
||||
if (elm == null)
|
||||
elm = document.body;
|
||||
elm.focus();
|
||||
if (this.contains(document.activeElement))
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
// Specify the menu's accessible name
|
||||
setName(name) {
|
||||
this.name = name || "";
|
||||
this.localize();
|
||||
// Trap pointer events
|
||||
this.addEventListener("pointerdown", e=>{
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Blur event capture
|
||||
onblur(e) {
|
||||
if (this.contains(e.relatedTarget))
|
||||
return;
|
||||
if (this.children.length > 0)
|
||||
this.children[0].element.setAttribute("tabindex", "0");
|
||||
if (this.expanded != null)
|
||||
this.expanded.setExpanded(false);
|
||||
}
|
||||
|
||||
// Focus event capture
|
||||
onfocus(e) {
|
||||
|
||||
// Configure tabstop on the first menu
|
||||
if (this.children.length > 0)
|
||||
this.children[0].element.setAttribute("tabindex", "-1");
|
||||
|
||||
// Retain a reference to the previously focused element
|
||||
if (this.contains(e.relatedTarget))
|
||||
return;
|
||||
let from = e.relatedTarget;
|
||||
if (from == null)
|
||||
from = document.body;
|
||||
if ("component" in from)
|
||||
from = from.component;
|
||||
this.lastFocus = from;
|
||||
}
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let text = this.name;
|
||||
if (this.application)
|
||||
text = this.application.translate(text, this);
|
||||
this.element.setAttribute("aria-label", text);
|
||||
// Replacement behavior for parent.add()
|
||||
addedHook(parent) {
|
||||
this.setAttribute("aria-labelledby", parent.id);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// MenuSeparator //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Separator between groups of menu items
|
||||
class MenuSeparator extends Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(gui, options) {
|
||||
super(gui, options, {
|
||||
className: "tk tk-menu-separator",
|
||||
role : "separator",
|
||||
tagName : "div"
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// MenuItem //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Individual menu selection
|
||||
class MenuItem extends Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(gui, options) {
|
||||
super(gui, options, {
|
||||
className: "tk tk-menu-item",
|
||||
focusable: true,
|
||||
tabStop : false,
|
||||
tagName : "div"
|
||||
});
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.isEnabled = null;
|
||||
this.isExpanded = false;
|
||||
this.menu = null;
|
||||
this.menuBar = null;
|
||||
this.text = null;
|
||||
this.type = null;
|
||||
|
||||
// Configure element
|
||||
this.contents = document.createElement("div");
|
||||
this.append(this.contents);
|
||||
this.eicon = document.createElement("div");
|
||||
this.eicon.className = "tk tk-icon";
|
||||
this.contents.append(this.eicon);
|
||||
this.etext = document.createElement("div");
|
||||
this.etext.className = "tk tk-text";
|
||||
this.contents.append(this.etext);
|
||||
|
||||
// Configure event handlers
|
||||
this.addEventListener("blur" , e=>this.onBlur (e));
|
||||
this.addEventListener("keydown" , e=>this.onKeyDown (e));
|
||||
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
|
||||
this.addEventListener("pointermove", e=>this.onPointerMove(e));
|
||||
this.addEventListener("pointerup" , e=>this.onPointerUp (e));
|
||||
|
||||
// Configure widget
|
||||
this.gui.localize(this);
|
||||
this.setEnabled("enabled" in options ? !!options.enabled : true);
|
||||
this.setId (Toolkit.id());
|
||||
this.setText (options.text);
|
||||
this.setType (options.type, options.checked);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Focus lost
|
||||
onBlur(e) {
|
||||
|
||||
// An item in a different menu is receiving focus
|
||||
if (this.menu != null) {
|
||||
if (
|
||||
!this .contains(e.relatedTarget) &&
|
||||
!this.menu.contains(e.relatedTarget)
|
||||
) this.setExpanded(false);
|
||||
}
|
||||
|
||||
// Item is an action
|
||||
else if (e.component == this)
|
||||
this.element.classList.remove("active");
|
||||
|
||||
// Simulate a bubbling event sequence
|
||||
if (this.parent)
|
||||
this.parent.onBlur(e);
|
||||
}
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
|
||||
case "ArrowDown":
|
||||
|
||||
// Error checking
|
||||
if (!this.parent)
|
||||
break;
|
||||
|
||||
// Top-level: open the menu and focus its first item
|
||||
if (this.parent == this.menuBar) {
|
||||
if (this.menu == null)
|
||||
return;
|
||||
this.setExpanded(true);
|
||||
this.listItems()[0].focus();
|
||||
}
|
||||
|
||||
// Sub-menu: cycle to the next sibling
|
||||
else {
|
||||
let items = this.parent.listItems();
|
||||
items[(items.indexOf(this) + 1) % items.length].focus();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "ArrowLeft":
|
||||
|
||||
// Error checking
|
||||
if (!this.parent)
|
||||
break;
|
||||
|
||||
// Sub-menu: close and focus parent
|
||||
if (
|
||||
this.parent != this.menuBar &&
|
||||
this.parent.parent != this.menuBar
|
||||
) {
|
||||
this.parent.setExpanded(false);
|
||||
this.parent.focus();
|
||||
}
|
||||
|
||||
// Top-level: cycle to previous sibling
|
||||
else {
|
||||
let menu = this.parent == this.menuBar ?
|
||||
this : this.parent;
|
||||
let items = this.menuBar.listItems();
|
||||
let prev = items[(items.indexOf(menu) +
|
||||
items.length - 1) % items.length];
|
||||
if (menu.isExpanded)
|
||||
prev.setExpanded(true);
|
||||
prev.focus();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "ArrowRight":
|
||||
|
||||
// Error checking
|
||||
if (!this.parent)
|
||||
break;
|
||||
|
||||
// Sub-menu: open the menu and focus its first item
|
||||
if (this.menu != null && this.parent != this.menuBar) {
|
||||
this.setExpanded(true);
|
||||
(this.listItems()[0] || this).focus();
|
||||
}
|
||||
|
||||
// Top level: cycle to next sibling
|
||||
else {
|
||||
let menu = this;
|
||||
while (menu.parent != this.menuBar)
|
||||
menu = menu.parent;
|
||||
let expanded = this.menuBar.expandedMenu() != null;
|
||||
let items = this.menuBar.listItems();
|
||||
let next = items[(items.indexOf(menu) + 1) % items.length];
|
||||
next.focus();
|
||||
if (expanded)
|
||||
next.setExpanded(true);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "ArrowUp":
|
||||
|
||||
// Error checking
|
||||
if (!this.parent)
|
||||
break;
|
||||
|
||||
// Top-level: open the menu and focus its last item
|
||||
if (this.parent == this.menuBar) {
|
||||
if (this.menu == null)
|
||||
return;
|
||||
this.setExpanded(true);
|
||||
let items = this.listItems();
|
||||
(items[items.length - 1] || this).focus();
|
||||
}
|
||||
|
||||
// Sub-menu: cycle to previous sibling
|
||||
else {
|
||||
let items = this.parent.listItems();
|
||||
items[(items.indexOf(this) +
|
||||
items.length - 1) % items.length].focus();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "End":
|
||||
{
|
||||
|
||||
// Error checking
|
||||
if (!this.parent)
|
||||
break;
|
||||
|
||||
// Focus last sibling
|
||||
let expanded = this.isExpanded &&
|
||||
this.parent == this.menuBar;
|
||||
let items = this.parent.listItems();
|
||||
let last = items[items.length - 1] || this;
|
||||
last.focus();
|
||||
if (expanded)
|
||||
last.setExpanded(true);
|
||||
}
|
||||
break;
|
||||
|
||||
case "Enter":
|
||||
case " ":
|
||||
|
||||
// Do nothing
|
||||
if (!this.isEnabled)
|
||||
break;
|
||||
|
||||
// Action item: activate the menu item
|
||||
if (this.menu == null)
|
||||
this.activate(this.type == "check" && e.key == " ");
|
||||
|
||||
// Sub-menu: open the menu and focus its first item
|
||||
else {
|
||||
this.setExpanded(true);
|
||||
let items = this.listItems();
|
||||
if (items[0])
|
||||
items[0].focus();
|
||||
}
|
||||
break;
|
||||
|
||||
case "Escape":
|
||||
|
||||
// Error checking
|
||||
if (!this.parent)
|
||||
break;
|
||||
|
||||
// Top-level (not specified by WAI-ARIA)
|
||||
if (this.parent == this.menuBar) {
|
||||
if (this.isExpanded)
|
||||
this.setExpanded(false);
|
||||
else this.menuBar.exit();
|
||||
}
|
||||
|
||||
// Sub-menu: close and focus parent
|
||||
else {
|
||||
this.parent.setExpanded(false);
|
||||
this.parent.focus();
|
||||
}
|
||||
break;
|
||||
|
||||
case "Home":
|
||||
{
|
||||
|
||||
// Error checking
|
||||
if (!this.parent)
|
||||
break;
|
||||
|
||||
// Focus first sibling
|
||||
let expanded = this.isExpanded &&
|
||||
this.parent == this.menuBar;
|
||||
let first = this.parent.listItems()[0] || this;
|
||||
first.focus();
|
||||
if (expanded)
|
||||
first.setExpanded(true);
|
||||
}
|
||||
break;
|
||||
|
||||
// Do not handle the event
|
||||
default: return;
|
||||
}
|
||||
|
||||
// The event was handled
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Pointer press
|
||||
onPointerDown(e) {
|
||||
this.focus();
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
!this.isEnabled ||
|
||||
this.element.hasPointerCapture(e.pointerId) ||
|
||||
e.button != 0
|
||||
) return;
|
||||
|
||||
// Configure event
|
||||
if (this.menu == null)
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Configure component
|
||||
if (this.menu != null)
|
||||
this.setExpanded(!this.isExpanded);
|
||||
else this.element.classList.add("active");
|
||||
}
|
||||
|
||||
// Pointer move
|
||||
onPointerMove(e) {
|
||||
|
||||
// Hovering over a menu when a sibling menu is already open
|
||||
let expanded = this.parent && this.parent.expandedMenu();
|
||||
if (this.menu != null && expanded != null && expanded != this) {
|
||||
|
||||
// Configure component
|
||||
this.setExpanded(true);
|
||||
this.focus();
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Not dragging
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Not an action item
|
||||
if (this.menu != null)
|
||||
return;
|
||||
|
||||
// Check if the cursor is within the bounds of the component
|
||||
this.element.classList[
|
||||
Toolkit.isInside(this.element, e) ? "add" : "remove"]("active");
|
||||
}
|
||||
|
||||
// Pointer release
|
||||
onPointerUp(e) {
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
!this.isEnabled ||
|
||||
e.button != 0 ||
|
||||
(this.parent && this.parent.hasFocus() ?
|
||||
this.menu != null :
|
||||
!this.element.hasPointerCapture(e.pointerId)
|
||||
)
|
||||
) return;
|
||||
|
||||
// Configure event
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Item is an action
|
||||
let bounds = this.getBounds();
|
||||
if (this.menu == null && Toolkit.isInside(this.element, e))
|
||||
this.activate();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Invoke an action command
|
||||
activate(noExit) {
|
||||
if (this.menu != null)
|
||||
return;
|
||||
|
||||
if (this.type == "check")
|
||||
this.setChecked(!this.isChecked);
|
||||
|
||||
if (!noExit)
|
||||
this.menuBar.exit();
|
||||
|
||||
this.element.dispatchEvent(Toolkit.event("action", this));
|
||||
}
|
||||
|
||||
// Add a separator between groups of menu items
|
||||
addSeparator(options) {
|
||||
let sep = new Toolkit.MenuSeparator(this, options);
|
||||
this.add(sep);
|
||||
return sep;
|
||||
}
|
||||
|
||||
// Produce a list of child items
|
||||
listItems(invisible) {
|
||||
return this.children.filter(c=>
|
||||
c instanceof Toolkit.MenuItem &&
|
||||
(invisible || c.isVisible())
|
||||
);
|
||||
}
|
||||
|
||||
// Specify whether the menu item is checked
|
||||
setChecked(checked) {
|
||||
if (this.type != "check")
|
||||
return;
|
||||
this.isChecked = !!checked;
|
||||
this.setAttribute("aria-checked", this.isChecked);
|
||||
}
|
||||
|
||||
// Specify whether the menu item can be activated
|
||||
setEnabled(enabled) {
|
||||
this.isEnabled = enabled = !!enabled;
|
||||
this.setAttribute("aria-disabled", enabled ? null : "true");
|
||||
if (!enabled)
|
||||
this.setExpanded(false);
|
||||
}
|
||||
|
||||
// Specify whether the sub-menu is open
|
||||
setExpanded(expanded) {
|
||||
|
||||
// State is not changing
|
||||
expanded = !!expanded;
|
||||
if (this.menu == null || expanded === this.isExpanded)
|
||||
return;
|
||||
|
||||
// Position the sub-menu
|
||||
if (expanded) {
|
||||
let bndGUI = this.gui .getBounds();
|
||||
let bndMenu = this.menu.getBounds();
|
||||
let bndThis = this .getBounds();
|
||||
let bndParent = !this.parent ? bndThis : (
|
||||
this.parent == this.menuBar ? this.parent : this.parent.menu
|
||||
).getBounds();
|
||||
this.menu.element.style.left = Math.max(0,
|
||||
Math.min(
|
||||
(this.parent && this.parent == this.menuBar ?
|
||||
bndThis.left : bndThis.right) - bndParent.left,
|
||||
bndGUI.right - bndMenu.width
|
||||
)
|
||||
) + "px";
|
||||
this.menu.element.style.top = Math.max(0,
|
||||
Math.min(
|
||||
(this.parent && this.parent == this.menuBar ?
|
||||
bndThis.bottom : bndThis.top) - bndParent.top,
|
||||
bndGUI.bottom - bndMenu.height
|
||||
)
|
||||
) + "px";
|
||||
}
|
||||
|
||||
// Close all open sub-menus
|
||||
else for (let child of this.listItems())
|
||||
child.setExpanded(false);
|
||||
|
||||
// Configure component
|
||||
this.isExpanded = expanded;
|
||||
this.setAttribute("aria-expanded", expanded);
|
||||
this.menu.setVisible(expanded);
|
||||
if (expanded)
|
||||
this.element.classList.add("active");
|
||||
else this.element.classList.remove("active");
|
||||
}
|
||||
|
||||
// Specify the widget's display text
|
||||
setText(text) {
|
||||
this.text = (text || "").toString().trim();
|
||||
this.translate();
|
||||
}
|
||||
|
||||
// Specify what kind of menu item this is
|
||||
setType(type, arg) {
|
||||
this.type = type = (type || "").toString().trim() || "normal";
|
||||
switch (type) {
|
||||
case "check":
|
||||
this.setAttribute("role", "menuitemcheckbox");
|
||||
this.setChecked(arg);
|
||||
break;
|
||||
default: // normal
|
||||
this.setAttribute("role", "menuitem");
|
||||
this.setAttribute("aria-checked", null);
|
||||
break;
|
||||
}
|
||||
if (this.parent && "checkIcons" in this.parent)
|
||||
this.parent.checkIcons();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Replacement behavior for add()
|
||||
addHook(component) {
|
||||
|
||||
// Convert to sub-menu
|
||||
if (this.menu == null) {
|
||||
this.menu = new Toolkit.Menu(this);
|
||||
this.after(this.menu);
|
||||
this.setAttribute("aria-haspopup", "menu");
|
||||
this.setAttribute("aria-expanded", "false");
|
||||
if (this.parent && "checkIcons" in this.parent)
|
||||
this.parent.checkIcons();
|
||||
}
|
||||
|
||||
// Add the child component
|
||||
component.menuBar = this.menuBar;
|
||||
this.menu.append(component);
|
||||
if (component instanceof Toolkit.MenuItem && component.menu != null)
|
||||
this.menu.append(component.menu);
|
||||
|
||||
// Configure icon mode
|
||||
this.checkIcons();
|
||||
}
|
||||
|
||||
// Check whether any child menu items contain icons
|
||||
checkIcons() {
|
||||
if (this.menu == null)
|
||||
return;
|
||||
if (this.children.filter(c=>
|
||||
c instanceof Toolkit.MenuItem &&
|
||||
c.menu == null &&
|
||||
c.type != "normal"
|
||||
).length != 0)
|
||||
this.menu.element.classList.add("icons");
|
||||
else this.menu.element.classList.remove("icons");
|
||||
}
|
||||
|
||||
// Replacement behavior for remove()
|
||||
removeHook(component) {
|
||||
|
||||
// Remove the child component
|
||||
component.element.remove();
|
||||
if (component instanceof Toolkit.MenuItem && component.menu != null)
|
||||
component.menu.element.remove();
|
||||
|
||||
// Convert to action item
|
||||
if (this.children.length == 0) {
|
||||
this.menu.element.remove();
|
||||
this.menu = null;
|
||||
this.setAttribute("aria-haspopup", null);
|
||||
this.setAttribute("aria-expanded", "false");
|
||||
if (this.parent && "checkIcons" in this.parent)
|
||||
this.parent.checkIcons();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Regenerate localized display text
|
||||
translate() {
|
||||
super.translate();
|
||||
if (!("contents" in this))
|
||||
return;
|
||||
this.etext.innerText = this.gui.translate(this.text, this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Retrieve the currently expanded sub-menu, if any
|
||||
expandedMenu() {
|
||||
return this.children.filter(c=>c.isExpanded)[0] || null;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// MenuBar //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Application menu bar
|
||||
class MenuBar extends Component {
|
||||
static Component = Component;
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(gui, options) {
|
||||
super(gui, options, {
|
||||
className: "tk tk-menu-bar",
|
||||
focusable: false,
|
||||
tagName : "div",
|
||||
tabStop : true,
|
||||
role : "menubar",
|
||||
style : {
|
||||
position: "relative",
|
||||
zIndex : "1"
|
||||
}
|
||||
});
|
||||
|
||||
// Configure instance fields
|
||||
this.focusTarget = null;
|
||||
this.menuBar = this;
|
||||
|
||||
// Configure event handlers
|
||||
this.addEventListener("blur" , e=>this.onBlur (e), true);
|
||||
this.addEventListener("focus" , e=>this.onFocus (e), true);
|
||||
this.addEventListener("keydown", e=>this.onKeyDown(e), true);
|
||||
|
||||
// Configure widget
|
||||
this.gui.localize(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Focus lost
|
||||
onBlur(e) {
|
||||
if (this.contains(e.relatedTarget))
|
||||
return;
|
||||
let items = this.listItems();
|
||||
if (items[0])
|
||||
items[0].setFocusable(true, true);
|
||||
let menu = this.expandedMenu();
|
||||
if (menu != null)
|
||||
menu.setExpanded(false);
|
||||
}
|
||||
|
||||
// Focus gained
|
||||
onFocus(e) {
|
||||
if (this.contains(e.relatedTarget))
|
||||
return;
|
||||
let items = this.listItems();
|
||||
if (items[0])
|
||||
items[0].setFocusable(true, false);
|
||||
this.focusTarget = e.relatedTarget;
|
||||
}
|
||||
|
||||
// Key pressed
|
||||
onKeyDown(e) {
|
||||
if (e.key != "Tab")
|
||||
return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.exit();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Produce a list of child items
|
||||
listItems(invisible) {
|
||||
return this.children.filter(c=>
|
||||
c instanceof Toolkit.MenuItem &&
|
||||
(invisible || c.isVisible())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Replacement behavior for add()
|
||||
addHook(component) {
|
||||
component.menuBar = this.menuBar;
|
||||
this.append(component);
|
||||
if (component instanceof Toolkit.MenuItem && component.menu != null)
|
||||
this.append(component.menu);
|
||||
let items = this.listItems();
|
||||
if (items[0])
|
||||
items[0].setFocusable(true, true);
|
||||
}
|
||||
|
||||
// Return control to the application
|
||||
exit() {
|
||||
this.onBlur({ relatedTarget: null });
|
||||
if (this.focusTarget)
|
||||
this.focusTarget.focus();
|
||||
else document.activeElement.blur();
|
||||
}
|
||||
|
||||
// Replacement behavior for remove()
|
||||
removeHook(component) {
|
||||
component.element.remove();
|
||||
if (component instanceof Toolkit.MenuItem && component.menu != null)
|
||||
component.menu.element.remove();
|
||||
let items = this.listItems();
|
||||
if (items[0])
|
||||
items[0].setFocusable(true, true);
|
||||
}
|
||||
|
||||
// Update the global Toolkit object
|
||||
static setToolkit(toolkit) {
|
||||
Toolkit = toolkit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Retrieve the currently expanded menu, if any
|
||||
expandedMenu() {
|
||||
return this.children.filter(c=>c.isExpanded)[0] || null;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
export { Menu, MenuBar, MenuItem, MenuSeparator };
|
||||
|
|
|
@ -1,320 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Selection within a Menu
|
||||
Toolkit.MenuItem = class MenuItem extends Toolkit.Panel {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, options);
|
||||
|
||||
// Configure instance fields
|
||||
this.clickListeners = [];
|
||||
this.enabled = "enabled" in options ? !!options.enabled : true;
|
||||
this.icon = options.icon || null;
|
||||
this.text = options.text || "";
|
||||
this.shortcut = options.shortcut || null;
|
||||
|
||||
// Configure base element
|
||||
this.setLayout("flex", {});
|
||||
this.element.setAttribute("role", "menuitem");
|
||||
this.element.setAttribute("tabindex", "-1");
|
||||
this.element.addEventListener("keydown", e=>this.onkeydown(e));
|
||||
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
||||
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
|
||||
this.element.addEventListener("pointerup" , e=>this.onpointerup (e));
|
||||
|
||||
// Configure display text element
|
||||
this.textElement = document.createElement("div");
|
||||
this.textElement.id = Toolkit.id();
|
||||
this.textElement.style.cursor = "default";
|
||||
this.textElement.style.flexGrow = "1";
|
||||
this.textElement.style.userSelect = "none";
|
||||
this.textElement.setAttribute("name", "text");
|
||||
this.element.appendChild(this.textElement);
|
||||
this.element.setAttribute("aria-labelledby", this.textElement.id);
|
||||
|
||||
// Configure properties
|
||||
this.setEnabled(this.enabled);
|
||||
this.setText (this.text);
|
||||
this.application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a callback for click events
|
||||
addClickListener(listener) {
|
||||
if (this.clickListeners.indexOf(listener) == -1)
|
||||
this.clickListeners.push(listener);
|
||||
}
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the item's display text
|
||||
getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
// Determine whether the item is enabled
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
// Specify whether the item is enabled
|
||||
setEnabled(enabled) {
|
||||
this.enabled = enabled = !!enabled;
|
||||
if (enabled)
|
||||
this.element.removeAttribute("disabled");
|
||||
else this.element.setAttribute("disabled", "");
|
||||
}
|
||||
|
||||
// Specify the item's display text
|
||||
setText(text) {
|
||||
this.text = text || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Configure this component to be a child of its parent
|
||||
child() {
|
||||
this.menuBar = this.parent;
|
||||
while (!(this.menuBar instanceof Toolkit.MenuBar))
|
||||
this.menuBar = this.menuBar.parent;
|
||||
this.menuItem = this instanceof Toolkit.Menu ? this : this.parent;
|
||||
while (!(this.menuItem instanceof Toolkit.Menu))
|
||||
this.menuItem = this.menuItem.parent;
|
||||
this.menuTop = this instanceof Toolkit.Menu ? this : this.parent;
|
||||
while (this.menuTop.parent != this.menuBar)
|
||||
this.menuTop = this.menuTop.parent;
|
||||
}
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let text = this.text;
|
||||
if (this.application)
|
||||
text = this.application.translate(text, this);
|
||||
this.textElement.innerText = text;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// The menu item was activated
|
||||
activate(e) {
|
||||
if (!this.enabled)
|
||||
return;
|
||||
this.menuBar.restoreFocus();
|
||||
for (let listener of this.clickListeners)
|
||||
listener(e, this);
|
||||
}
|
||||
|
||||
// Key press event handler
|
||||
onkeydown(e) {
|
||||
let index;
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
|
||||
// Activate the item
|
||||
case " ":
|
||||
case "Enter":
|
||||
this.activate(e);
|
||||
break;
|
||||
|
||||
// Select the next item
|
||||
case "ArrowDown":
|
||||
index = this.parent.nextChild(
|
||||
this.parent.children.indexOf(this));
|
||||
if (index != -1)
|
||||
this.parent.children[index].focus();
|
||||
break;
|
||||
|
||||
// Conditional
|
||||
case "ArrowLeft":
|
||||
|
||||
// Move to the previous menu in the menu bar
|
||||
if (this.menuItem.parent == this.menuBar) {
|
||||
index = this.menuBar.previousChild(
|
||||
this.menuBar.children.indexOf(this.menuItem));
|
||||
if (index != -1) {
|
||||
let menu = this.menuBar.children[index];
|
||||
if (menu != this.menuTop) {
|
||||
if (this.menuBar.expanded != null)
|
||||
menu.activate(true);
|
||||
else menu.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close the containing submenu
|
||||
else {
|
||||
this.menuItem.setExpanded(false);
|
||||
this.menuItem.focus();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Move to the next menu in the menu bar
|
||||
case "ArrowRight":
|
||||
index = this.menuBar.nextChild(
|
||||
this.menuBar.children.indexOf(this.menuTop));
|
||||
if (index != -1) {
|
||||
let menu = this.menuBar.children[index];
|
||||
if (menu != this.menuTop) {
|
||||
if (this.menuBar.expanded != null)
|
||||
menu.activate(true);
|
||||
else menu.focus();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// Select the previous item
|
||||
case "ArrowUp":
|
||||
index = this.parent.previousChild(
|
||||
this.parent.children.indexOf(this));
|
||||
if (index != -1)
|
||||
this.parent.children[index].focus();
|
||||
break;
|
||||
|
||||
// Conditional
|
||||
case "End":
|
||||
|
||||
// Select the last menu in the menu bar
|
||||
if (this.parent == this.menuBar) {
|
||||
index = this.menuBar.previousChild(
|
||||
this.menuBar.children.length);
|
||||
if (index != -1) {
|
||||
let menu = this.menuBar.children[index];
|
||||
if (menu != this.menuTop) {
|
||||
if (this.menuBar.expanded != null)
|
||||
menu.activate(true);
|
||||
else menu.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Select the last item in the menu
|
||||
else {
|
||||
index = this.menuItem.previousChild(
|
||||
this.menuItem.children.length);
|
||||
if (index != -1)
|
||||
this.menuItem.children[index].focus();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Return focus to the original element
|
||||
case "Escape":
|
||||
if (this.menuBar.expanded != null)
|
||||
this.menuBar.expanded.setExpanded(false);
|
||||
this.menuBar.restoreFocus();
|
||||
break;
|
||||
|
||||
// Conditional
|
||||
case "Home":
|
||||
|
||||
// Select the first menu in the menu bar
|
||||
if (this.parent == this.menuBar) {
|
||||
index = this.menuBar.nextChild(-1);
|
||||
if (index != -1) {
|
||||
let menu = this.menuBar.children[index];
|
||||
if (menu != this.menuTop) {
|
||||
if (this.menuBar.expanded != null)
|
||||
menu.activate(true);
|
||||
else menu.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Select the last item in the menu
|
||||
else {
|
||||
index = this.menuItem.nextChild(-1);
|
||||
if (index != -1)
|
||||
this.menuItem.children[index].focus();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Pointer down event handler
|
||||
onpointerdown(e) {
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Configure focus
|
||||
if (this.enabled)
|
||||
this.focus();
|
||||
else return;
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0 || this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Configure element
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
this.element.setAttribute("active", "");
|
||||
}
|
||||
|
||||
// Pointer move event handler
|
||||
onpointermove(e) {
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e.pointerid))
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let bounds = this.getBounds();
|
||||
let active =
|
||||
e.x >= bounds.x && e.x < bounds.x + bounds.width &&
|
||||
e.y >= bounds.y && e.y < bounds.y + bounds.height
|
||||
;
|
||||
|
||||
// Configure element
|
||||
if (active)
|
||||
this.element.setAttribute("active", "");
|
||||
else this.element.removeAttribute("active");
|
||||
}
|
||||
|
||||
// Pointer up event handler
|
||||
onpointerup(e) {
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Configure element
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
|
||||
// Activate the menu item if it is active
|
||||
if (!this.element.hasAttribute("active"))
|
||||
return;
|
||||
this.element.removeAttribute("active");
|
||||
this.activate(e);
|
||||
}
|
||||
|
||||
};
|
|
@ -1,354 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Box that can contain other components
|
||||
Toolkit.Panel = class Panel extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, "div", options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.alignCross = "start";
|
||||
this.alignMain = "start";
|
||||
this.application = application;
|
||||
this.children = [];
|
||||
this.columns = null;
|
||||
this.direction = "row";
|
||||
this.focusable = "focusable" in options ? !!options.focusable:false;
|
||||
this.hollow = "hollow" in options ? !!options.hollow : true;
|
||||
this.layout = null;
|
||||
this.name = options.name || "";
|
||||
this.overflowX = options.overflowX || "hidden";
|
||||
this.overflowY = options.overflowY || "hidden";
|
||||
this.rows = null;
|
||||
this.wrap = false;
|
||||
|
||||
// Configure properties
|
||||
this.setFocusable(this.focusable);
|
||||
this.setHollow (this.hollow);
|
||||
this.setLayout (options.layout || null, options);
|
||||
this.setName (this.name);
|
||||
this.setOverflow (this.overflowX, this.overflowY);
|
||||
if (this.application && this.focusable)
|
||||
this.application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a component as a child of this container
|
||||
add(component, index) {
|
||||
|
||||
// Determine the ordinal position of the element within the container
|
||||
index = !(typeof index == "number") ? this.children.length :
|
||||
Math.floor(Math.min(Math.max(0, index), this.children.length));
|
||||
|
||||
// Add the component to the container
|
||||
let ref = this.children[index] || null;
|
||||
component.parent = this;
|
||||
this.element.insertBefore(component.element,
|
||||
ref == null ? null : ref.element);
|
||||
this.children.splice(index, 0, component);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
if (this.focusable)
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the component's accessible name
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
// Determine whether the component is focusable
|
||||
isFocusable() {
|
||||
return this.focusable;
|
||||
}
|
||||
|
||||
// Determine whether the component is hollow
|
||||
isHollow() {
|
||||
return this.hollow;
|
||||
}
|
||||
|
||||
// Create a Button and associate it with the application
|
||||
newButton(options) {
|
||||
return new Toolkit.Button(this.application, options);
|
||||
}
|
||||
|
||||
// Create a CheckBox and associate it with the application
|
||||
newCheckBox(options) {
|
||||
return new Toolkit.CheckBox(this.application, options);
|
||||
}
|
||||
|
||||
// Create a Label and associate it with the application
|
||||
newLabel(options) {
|
||||
return new Toolkit.Label(this.application, options);
|
||||
}
|
||||
|
||||
// Create a ListBox and associate it with the application
|
||||
newListBox(options) {
|
||||
return new Toolkit.ListBox(this.application, options);
|
||||
}
|
||||
|
||||
// Create a MenuBar and associate it with the application
|
||||
newMenuBar(options) {
|
||||
return new Toolkit.MenuBar(this.application, options);
|
||||
}
|
||||
|
||||
// Create a Panel and associate it with the application
|
||||
newPanel(options) {
|
||||
return new Toolkit.Panel(this.application, options);
|
||||
}
|
||||
|
||||
// Create a RadioButton and associate it with the application
|
||||
newRadioButton(options) {
|
||||
return new Toolkit.RadioButton(this.application, options);
|
||||
}
|
||||
|
||||
// Create a Splitter and associate it with the application
|
||||
newSplitter(options) {
|
||||
return new Toolkit.Splitter(this.application, options);
|
||||
}
|
||||
|
||||
// Create a TextBox and associate it with the application
|
||||
newTextBox(options) {
|
||||
return new Toolkit.TextBox(this.application, options);
|
||||
}
|
||||
|
||||
// Create a Window and associate it with the application
|
||||
newWindow(options) {
|
||||
return new Toolkit.Window(this.application, options);
|
||||
}
|
||||
|
||||
// Determine the index of the next visible child
|
||||
nextChild(index) {
|
||||
for (let x = 0; x <= this.children.length; x++) {
|
||||
index = (index + 1) % this.children.length;
|
||||
let comp = this.children[index];
|
||||
if (comp.isVisible())
|
||||
return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Determine the index of the previous visible child
|
||||
previousChild(index) {
|
||||
for (let x = 0; x <= this.children.length; x++) {
|
||||
index = (index + this.children.length - 1) % this.children.length;
|
||||
let comp = this.children[index];
|
||||
if (comp.isVisible())
|
||||
return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Remove a component from the container
|
||||
remove(component) {
|
||||
|
||||
// Locate the component in the children
|
||||
let index = this.children.indexOf(component);
|
||||
if (index == -1)
|
||||
return;
|
||||
|
||||
// Remove the component
|
||||
component.parent = null;
|
||||
component.element.remove();
|
||||
this.children.splice(index, 1);
|
||||
}
|
||||
|
||||
// Specify whether the component is focusable
|
||||
setFocusable(focusable) {
|
||||
this.focusable = focusable = !!focusable;
|
||||
if (focusable) {
|
||||
this.element.setAttribute("tabindex", "0");
|
||||
this.application && this.application.addComponent(this);
|
||||
} else {
|
||||
this.element.removeAttribute("aria-label");
|
||||
this.element.removeAttribute("tabindex");
|
||||
this.application && this.application.removeComponent(this);
|
||||
}
|
||||
}
|
||||
|
||||
// Specify whether the component is hollow
|
||||
setHollow(hollow) {
|
||||
this.hollow = hollow = !!hollow;
|
||||
if (hollow) {
|
||||
this.element.style.minHeight = "0";
|
||||
this.element.style.minWidth = "0";
|
||||
} else {
|
||||
this.element.style.removeProperty("min-height");
|
||||
this.element.style.removeProperty("min-width" );
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the element's layout
|
||||
setLayout(layout, options) {
|
||||
|
||||
// Configure instance fields
|
||||
this.layout = layout;
|
||||
|
||||
// Processing by layout
|
||||
options = options || {};
|
||||
switch (layout) {
|
||||
case "block" : this.setBlockLayout (options); break;
|
||||
case "desktop": this.setDesktopLayout(options); break;
|
||||
case "flex" : this.setFlexLayout (options); break;
|
||||
case "grid" : this.setGridLayout (options); break;
|
||||
default : this.setNullLayout (options); break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Specify the component's accessible name
|
||||
setName(name) {
|
||||
this.name = name || "";
|
||||
if (this.focusable)
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Configure the panel's overflow scrolling behavior
|
||||
setOverflow(x, y) {
|
||||
this.element.style.overflowX = this.overflowX = x || "hidden";
|
||||
this.element.style.overflowY = this.overflowY = y || this.overflowX;
|
||||
}
|
||||
|
||||
// Specify the semantic role of the panel
|
||||
setRole(role) {
|
||||
if (!role)
|
||||
this.element.removeAttribute("role");
|
||||
else this.element.setAttribute("role", "" + role);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Move a window to the foreground
|
||||
bringToFront(wnd) {
|
||||
for (let child of this.children)
|
||||
child.element.style.zIndex = child == wnd ? "1" : "0";
|
||||
}
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let name = this.name;
|
||||
if (this.application)
|
||||
name = this.application.translate(name, this);
|
||||
this.element.setAttribute("aria-label", name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Resize event handler
|
||||
onresize(desktop) {
|
||||
|
||||
// Error checking
|
||||
if (this.layout != "desktop")
|
||||
return;
|
||||
|
||||
// Ensure all child windows are visible in the viewport
|
||||
for (let wnd of this.children) {
|
||||
if (!wnd.isVisible())
|
||||
continue;
|
||||
let bounds = wnd.getBounds();
|
||||
wnd.contain(
|
||||
bounds.x - desktop.x,
|
||||
bounds.y - desktop.y,
|
||||
desktop, bounds
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Configure a block layout
|
||||
setBlockLayout(options) {
|
||||
|
||||
// Configure instance fields
|
||||
this.layout = "block";
|
||||
|
||||
// Configure element
|
||||
this.setDisplay("block");
|
||||
}
|
||||
|
||||
// Configure a desktop layout
|
||||
setDesktopLayout(options) {
|
||||
|
||||
// Configure instance fields
|
||||
this.layout = "desktop";
|
||||
|
||||
// Configure element
|
||||
this.setDisplay("block");
|
||||
this.element.style.position = "relative";
|
||||
if (this.resizeObserver == null)
|
||||
this.addResizeListener(b=>this.onresize(b));
|
||||
}
|
||||
|
||||
// Configure a flex layout
|
||||
setFlexLayout(options) {
|
||||
|
||||
// Configure instance fields
|
||||
this.alignCross = options.alignCross || "start";
|
||||
this.alignMain = options.alignMain || "start";
|
||||
this.direction = options.direction || this.direction;
|
||||
this.layout = "flex";
|
||||
this.wrap = !!options.wrap;
|
||||
|
||||
// Working variables
|
||||
let alignCross = this.alignCross;
|
||||
let alignMain = this.alignMain;
|
||||
if (alignCross == "start" || alignCross == "end")
|
||||
alignCross = "flex-" + alignCross;
|
||||
if (alignMain == "start" || alignMain == "end")
|
||||
alignMain = "flex-" + alignMain;
|
||||
|
||||
// Configure element
|
||||
this.setDisplay("flex");
|
||||
this.element.style.alignItems = alignCross;
|
||||
this.element.style.flexDirection = this.direction;
|
||||
this.element.style.justifyContent = alignMain;
|
||||
this.element.style.flexWrap = this.wrap ? "wrap" : "nowrap";
|
||||
}
|
||||
|
||||
// Configure a grid layout
|
||||
setGridLayout(options) {
|
||||
|
||||
// Configure instance fields
|
||||
this.columns = options.columns || null;
|
||||
this.layout = "grid";
|
||||
this.rows = options.rows || null;
|
||||
|
||||
// Configure element
|
||||
this.setDisplay("grid");
|
||||
if (this.columns == null)
|
||||
this.element.style.removeProperty("grid-template-columns");
|
||||
else this.element.style.gridTemplateColumns = this.columns;
|
||||
if (this.rows == null)
|
||||
this.element.style.removeProperty("grid-template-rows");
|
||||
else this.element.style.gridTemplateRows = this.rows;
|
||||
}
|
||||
|
||||
// Configure a null layout
|
||||
setNullLayout(options) {
|
||||
|
||||
// Configure instance fields
|
||||
this.layout = null;
|
||||
|
||||
// Configure element
|
||||
this.setDisplay(null);
|
||||
this.element.style.removeProperty("align-items" );
|
||||
this.element.style.removeProperty("flex-wrap" );
|
||||
this.element.style.removeProperty("grid-template-columns");
|
||||
this.element.style.removeProperty("grid-template-rows" );
|
||||
this.element.style.removeProperty("justify-content" );
|
||||
this.element.style.removeProperty("flex-direction" );
|
||||
}
|
||||
|
||||
};
|
|
@ -1,59 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Select-only radio button
|
||||
Toolkit.RadioButton = class RadioButton extends Toolkit.CheckBox {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.group = null;
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("role", "radio");
|
||||
|
||||
// Configure properties
|
||||
this.setGroup(options.group || null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Retrieve the enclosing ButtonGroup
|
||||
getGroup() {
|
||||
return this.group;
|
||||
}
|
||||
|
||||
// Specify whether the component is checked (overrides superclass)
|
||||
setChecked(checked, e) {
|
||||
checked = !!checked;
|
||||
if (e instanceof Event && !checked || checked == this.checked)
|
||||
return;
|
||||
|
||||
this.checked = checked;
|
||||
this.element.setAttribute("aria-checked", checked);
|
||||
if (this.group != null && e != this.group)
|
||||
this.group.setChecked(this);
|
||||
|
||||
if (e === undefined)
|
||||
return;
|
||||
for (let listener of this.changeListeners)
|
||||
listener(e);
|
||||
}
|
||||
|
||||
// Specify the enclosing ButtonGroup
|
||||
setGroup(group) {
|
||||
group = group || null;
|
||||
if (group == this.group)
|
||||
return;
|
||||
if (this.group != null)
|
||||
this.group.remove(this);
|
||||
this.group = group;
|
||||
if (group != null)
|
||||
group.add(this);
|
||||
}
|
||||
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -1,301 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Interactive splitter
|
||||
Toolkit.Splitter = class Splitter extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, "div", options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.component = options.component || null;
|
||||
this.dragPointer = null;
|
||||
this.dragPos = 0;
|
||||
this.dragSize = 0;
|
||||
this.orientation = options.orientation || "horizontal";
|
||||
this.name = options.name || "";
|
||||
this.edge = options.edge ||
|
||||
(this.orientation == "horizontal" ? "top" : "left");
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("role" , "separator");
|
||||
this.element.setAttribute("tabindex" , "0");
|
||||
this.element.setAttribute("aria-valuemin", "0");
|
||||
this.element.addEventListener("keydown" , e=>this.onkeydown (e));
|
||||
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
||||
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
|
||||
this.element.addEventListener("pointerup" , e=>this.onpointerup (e));
|
||||
|
||||
// Configure properties
|
||||
this.setComponent (this.component );
|
||||
this.setName (this.name );
|
||||
this.setOrientation(this.orientation);
|
||||
this.application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the component managed by this Splitter
|
||||
getComponent() {
|
||||
return this.component;
|
||||
}
|
||||
|
||||
// Retrieve the component's accessible name
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
// Retrieve the component's orientation
|
||||
getOrientation() {
|
||||
return this.orientation;
|
||||
}
|
||||
|
||||
// Determine the current and maximum separator values
|
||||
measure() {
|
||||
let max = 0;
|
||||
let now = 0;
|
||||
|
||||
// Mesure the component
|
||||
if (this.component != null && this.parent != null) {
|
||||
let component = this.component.getBounds();
|
||||
let bounds = this.getBounds();
|
||||
let panel = this.parent.getBounds();
|
||||
|
||||
// Horizontal Splitter
|
||||
if (this.orientation == "horizontal") {
|
||||
max = panel.height - bounds.height;
|
||||
now = Math.max(0, Math.min(max, component.height));
|
||||
this.component.setSize(null, now);
|
||||
}
|
||||
|
||||
// Vertical Splitter
|
||||
else {
|
||||
max = panel.width - bounds.width;
|
||||
now = Math.max(0, Math.min(max, component.width));
|
||||
this.component.setSize(now, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("aria-valuemax", max);
|
||||
this.element.setAttribute("aria-valuenow", now);
|
||||
}
|
||||
|
||||
// Specify the component managed by this Splitter
|
||||
setComponent(component) {
|
||||
this.component = component = component || null;
|
||||
this.element.setAttribute("aria-controls",
|
||||
component == null ? "" : component.id);
|
||||
this.measure();
|
||||
}
|
||||
|
||||
// Specify the component's accessible name
|
||||
setName(name) {
|
||||
this.name = name || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Specify the component's orientation
|
||||
setOrientation(orientation) {
|
||||
switch (orientation) {
|
||||
case "horizontal":
|
||||
this.orientation = "horizontal";
|
||||
this.setSize(null, 3, true);
|
||||
this.element.setAttribute("aria-orientation", "horizontal");
|
||||
this.element.style.cursor = "ew-resize";
|
||||
break;
|
||||
case "vertical":
|
||||
this.orientation = "vertical";
|
||||
this.setSize(3, null, true);
|
||||
this.element.setAttribute("aria-orientation", "vertical");
|
||||
this.element.style.cursor = "ns-resize";
|
||||
}
|
||||
this.measure();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let name = this.name;
|
||||
if (this.application) {
|
||||
name = this.application.translate(name, this);
|
||||
}
|
||||
this.element.setAttribute("aria-label", name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Key press event handler
|
||||
onkeydown(e) {
|
||||
|
||||
// Error checking
|
||||
if (this.component == null)
|
||||
return;
|
||||
|
||||
let pos = this.component.getBounds();
|
||||
let size = this .getBounds();
|
||||
let max = this.parent .getBounds();
|
||||
if (this.orientation == "horizontal") {
|
||||
max = max .height;
|
||||
pos = pos .height;
|
||||
size = size.height;
|
||||
} else {
|
||||
max = max .width;
|
||||
pos = pos .width;
|
||||
size = size.width;
|
||||
}
|
||||
if (this.edge == "top" || this.edge == "left")
|
||||
max -= size;
|
||||
|
||||
// Processing by key
|
||||
if (this.component != null) switch (e.key) {
|
||||
case "ArrowDown":
|
||||
switch (this.edge) {
|
||||
case "bottom":
|
||||
this.component.setSize(null, Math.min(max, pos - 6));
|
||||
break;
|
||||
case "top":
|
||||
this.component.setSize(null, Math.min(max, pos + 6));
|
||||
}
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
switch (this.edge) {
|
||||
case "left":
|
||||
this.component.setSize(Math.min(max, pos - 6), null);
|
||||
break;
|
||||
case "right":
|
||||
this.component.setSize(Math.min(max, pos + 6), null);
|
||||
}
|
||||
break;
|
||||
case "ArrowRight":
|
||||
switch (this.edge) {
|
||||
case "left":
|
||||
this.component.setSize(Math.min(max, pos + 6), null);
|
||||
break;
|
||||
case "right":
|
||||
this.component.setSize(Math.min(max, pos - 6), null);
|
||||
}
|
||||
break;
|
||||
case "ArrowUp":
|
||||
switch (this.edge) {
|
||||
case "bottom":
|
||||
this.component.setSize(null, Math.min(max, pos + 6));
|
||||
break;
|
||||
case "top":
|
||||
this.component.setSize(null, Math.min(max, pos - 6));
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
if (this.dragPointer === null)
|
||||
return;
|
||||
this.element.releasePointerCapture(this.dragPointer);
|
||||
this.dragPointer = null;
|
||||
if (this.orientation == "horizontal")
|
||||
this.component.setHeight(null, this.dragSize);
|
||||
else this.component.setWidth(this.dragSize, null);
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Pointer down event handler
|
||||
onpointerdown(e) {
|
||||
|
||||
// Request focus
|
||||
this.focus();
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
this.component == null ||
|
||||
e.button != 0 ||
|
||||
this.element.hasPointerCapture(e.pointerId)
|
||||
) return;
|
||||
|
||||
// Capture the pointer
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
this.dragPointer = e.pointerId;
|
||||
let bounds = this.component.getBounds();
|
||||
if (this.orientation == "horizontal") {
|
||||
this.dragPos = e.y;
|
||||
this.dragSize = bounds.height;
|
||||
} else {
|
||||
this.dragPos = e.x;
|
||||
this.dragSize = bounds.width;
|
||||
}
|
||||
}
|
||||
|
||||
// Pointer move event handler
|
||||
onpointermove(e) {
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
this.component == null ||
|
||||
!this.element.hasPointerCapture(e.pointerId)
|
||||
) return;
|
||||
|
||||
// Resize the component
|
||||
let bounds = this.getBounds();
|
||||
let panel = this.parent.getBounds();
|
||||
switch (this.edge) {
|
||||
case "bottom":
|
||||
this.component.setSize(null, Math.max(0, Math.min(
|
||||
this.dragSize - e.y + this.dragPos,
|
||||
panel.height - bounds.height
|
||||
)));
|
||||
break;
|
||||
case "left":
|
||||
this.component.setSize(Math.max(0, Math.min(
|
||||
this.dragSize + e.x - this.dragPos,
|
||||
panel.width - bounds.width
|
||||
)), null);
|
||||
break;
|
||||
case "right":
|
||||
this.component.setSize(Math.max(0, Math.min(
|
||||
this.dragSize - e.x + this.dragPos,
|
||||
panel.width - bounds.width
|
||||
)), null);
|
||||
break;
|
||||
case "top":
|
||||
this.component.setSize(null, Math.max(0, Math.min(
|
||||
this.dragSize + e.y - this.dragPos,
|
||||
panel.height - bounds.height
|
||||
)));
|
||||
break;
|
||||
}
|
||||
this.measure();
|
||||
}
|
||||
|
||||
// Pointer up event handler
|
||||
onpointerup(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
this.dragPointer = null;
|
||||
}
|
||||
|
||||
};
|
|
@ -1,138 +1,145 @@
|
|||
"use strict";
|
||||
import { Component } from /**/"./Component.js";
|
||||
let Toolkit;
|
||||
|
||||
Toolkit.TextBox = class TextBox extends Toolkit.Component {
|
||||
|
||||
constructor(application, options) {
|
||||
super(application, "input", options);
|
||||
options = options || {};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// TextBox //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Text entry field
|
||||
class TextBox extends Component {
|
||||
static Component = Component;
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(gui, options) {
|
||||
super(gui, options, {
|
||||
className: "tk tk-textbox",
|
||||
tagName : "input"
|
||||
});
|
||||
this.element.type = "text";
|
||||
this.setAttribute("spellcheck", "false");
|
||||
|
||||
// Configure instance fields
|
||||
this.changeListeners = [];
|
||||
this.commitListeners = [];
|
||||
this.enabled = "enabled" in options ? !!options.enabled : true;
|
||||
this.name = options.name || "";
|
||||
this.lastCommit = "";
|
||||
this.isEnabled = null;
|
||||
this.maxLength = null;
|
||||
this.pattern = null;
|
||||
|
||||
// Configure element
|
||||
this.element.size = 1;
|
||||
this.element.type = "text";
|
||||
this.element.addEventListener("blur" , e=>this.commit (e));
|
||||
this.element.addEventListener("input" , e=>this.onchange (e));
|
||||
this.element.addEventListener("keydown", e=>this.onkeydown(e));
|
||||
// Configure component
|
||||
options = options || {};
|
||||
this.setEnabled(!("enabled" in options) || options.enabled);
|
||||
if ("maxLength" in options)
|
||||
this.setMaxLength(options.maxLength);
|
||||
if ("pattern" in options)
|
||||
this.setPattern(options.pattern);
|
||||
this.setText (options.text);
|
||||
|
||||
// Configure properties
|
||||
this.setEnabled(this.enabled);
|
||||
this.setName (this.name );
|
||||
this.setText (options.text || "");
|
||||
this.application.addComponent(this);
|
||||
// Configure event handlers
|
||||
this.addEventListener("blur" , e=>this.commit ( ));
|
||||
this.addEventListener("pointerdown", e=>e.stopPropagation( ));
|
||||
this.addEventListener("keydown" , e=>this.onKeyDown (e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
case "ArrowLeft":
|
||||
case "ArrowRight":
|
||||
e.stopPropagation();
|
||||
return;
|
||||
case "Enter":
|
||||
this.commit();
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a callback for change events
|
||||
addChangeListener(listener) {
|
||||
if (this.changeListeners.indexOf(listener) == -1)
|
||||
this.changeListeners.push(listener);
|
||||
// Programmatically commit the text box
|
||||
commit() {
|
||||
this.event("action");
|
||||
}
|
||||
|
||||
// Add a callback for commit events
|
||||
addCommitListener(listener) {
|
||||
if (this.commitListeners.indexOf(listener) == -1)
|
||||
this.commitListeners.push(listener);
|
||||
}
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the component's accessible name
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
// Retrieve the component's display text
|
||||
// Retrieve the control's value
|
||||
getText() {
|
||||
return this.element.value;
|
||||
}
|
||||
|
||||
// Determine whether the component is enabled
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
// Specify whether the component is enabled
|
||||
// Specify whether the button can be activated
|
||||
setEnabled(enabled) {
|
||||
this.enabled = enabled = !!enabled;
|
||||
this.element.setAttribute("aria-disabled", !enabled);
|
||||
if (enabled)
|
||||
this.element.removeAttribute("disabled");
|
||||
else this.element.setAttribute("disabled", "");
|
||||
this.isEnabled = enabled = !!enabled;
|
||||
this.setAttribute("disabled", enabled ? null : "true");
|
||||
}
|
||||
|
||||
// Specify the component's accessible name
|
||||
setName(name) {
|
||||
this.name = name || "";
|
||||
this.localize();
|
||||
// Specify the maximum length of the text
|
||||
setMaxLength(length) {
|
||||
|
||||
// Remove limitation
|
||||
if (length === null) {
|
||||
this.maxLength = null;
|
||||
this.setAttribute("maxlength", null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Error checking
|
||||
if (typeof length != "number" || isNaN(length))
|
||||
return;
|
||||
|
||||
// Range checking
|
||||
length = Math.floor(length);
|
||||
if (length < 0)
|
||||
return;
|
||||
|
||||
// Configure component
|
||||
this.maxLength = length;
|
||||
this.setAttribute("maxlength", length);
|
||||
}
|
||||
|
||||
// Specify the component's display text
|
||||
setText(text) {
|
||||
text = !text && text !== 0 ? "" : "" + text;
|
||||
this.lastCommit = text;
|
||||
this.element.value = text;
|
||||
// Specify a regex pattern for valid text characters
|
||||
setPattern(pattern) {
|
||||
/*
|
||||
Disabled because user agents may not prevent invalid input
|
||||
|
||||
// Error checking
|
||||
if (pattern && typeof pattern != "string")
|
||||
return;
|
||||
|
||||
// Configure component
|
||||
this.pattern = pattern = pattern || null;
|
||||
this.setAttribute("pattern", pattern);
|
||||
*/
|
||||
}
|
||||
|
||||
// Specify the widget's display text
|
||||
setText(text = "") {
|
||||
this.element.value = text.toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let name = this.name;
|
||||
if (this.application)
|
||||
name = this.application.translate(name, this);
|
||||
this.element.setAttribute("aria-label", name);
|
||||
// Update the global Toolkit object
|
||||
static setToolkit(toolkit) {
|
||||
Toolkit = toolkit;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Input finalized
|
||||
commit(e) {
|
||||
let text = this.element.value || "";
|
||||
if (!this.enabled || text == this.lastCommit)
|
||||
return;
|
||||
this.lastCommit = text;
|
||||
for (let listener of this.commitListeners)
|
||||
listener(e, this);
|
||||
}
|
||||
|
||||
// Text changed event handler
|
||||
onchange(e) {
|
||||
e.stopPropagation();
|
||||
if (!this.enabled)
|
||||
return;
|
||||
for (let listener of this.changeListeners)
|
||||
listener(e, this);
|
||||
}
|
||||
|
||||
// Key press event handler
|
||||
onkeydown(e) {
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (!this.enabled)
|
||||
return;
|
||||
|
||||
// The Enter key was pressed
|
||||
if (e.key == "Enter")
|
||||
this.commit(e);
|
||||
}
|
||||
|
||||
};
|
||||
export { TextBox };
|
||||
|
|
|
@ -1,18 +1,334 @@
|
|||
"use strict";
|
||||
import { Component } from /**/"./Component.js";
|
||||
import { Button, CheckBox, Group, Radio } from /**/"./Button.js" ;
|
||||
import { Menu, MenuBar, MenuItem, MenuSeparator } from /**/"./MenuBar.js" ;
|
||||
import { ScrollBar, ScrollPane, SplitPane } from /**/"./ScrollBar.js";
|
||||
import { TextBox } from /**/"./TextBox.js" ;
|
||||
import { Desktop, Window } from /**/"./Window.js" ;
|
||||
|
||||
// Widget toolkit manager
|
||||
(globalThis.Toolkit = class Toolkit {
|
||||
|
||||
// Static initializer
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Toolkit //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Top-level user interface manager
|
||||
let Toolkit = globalThis.Toolkit = (class GUI extends Component {
|
||||
|
||||
static initializer() {
|
||||
|
||||
// Static fields
|
||||
Toolkit.lastId = 0;
|
||||
// Static state
|
||||
this.nextId = 0;
|
||||
|
||||
// Locale presets
|
||||
this.NO_LOCALE = { id: "(Null)" };
|
||||
|
||||
// Component classes
|
||||
this.components = [];
|
||||
Button .setToolkit(this); this.components.push(Button .Component);
|
||||
Component.setToolkit(this); this.components.push( Component);
|
||||
MenuBar .setToolkit(this); this.components.push(MenuBar .Component);
|
||||
ScrollBar.setToolkit(this); this.components.push(ScrollBar.Component);
|
||||
TextBox .setToolkit(this); this.components.push(TextBox .Component);
|
||||
Window .setToolkit(this); this.components.push(Window .Component);
|
||||
this.Button = Button;
|
||||
this.CheckBox = CheckBox;
|
||||
this.Component = Component;
|
||||
this.Desktop = Desktop;
|
||||
this.Group = Group;
|
||||
this.Menu = Menu;
|
||||
this.MenuBar = MenuBar;
|
||||
this.MenuItem = MenuItem;
|
||||
this.MenuSeparator = MenuSeparator;
|
||||
this.Radio = Radio;
|
||||
this.ScrollBar = ScrollBar;
|
||||
this.ScrollPane = ScrollPane;
|
||||
this.SplitPane = SplitPane;
|
||||
this.TextBox = TextBox;
|
||||
this.Window = Window;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Static Methods //////////////////////////////
|
||||
|
||||
// Monitor resize events on an element
|
||||
static addResizeListener(element, listener) {
|
||||
|
||||
// Establish a ResizeObserver
|
||||
if (!("resizeListeners" in element)) {
|
||||
element.resizeListeners = [];
|
||||
element.resizeObserver = new ResizeObserver(
|
||||
(e,o)=>element.dispatchEvent(this.event("resize")));
|
||||
element.resizeObserver.observe(element);
|
||||
}
|
||||
|
||||
// Associate the listener
|
||||
if (element.resizeListeners.indexOf(listener) == -1) {
|
||||
element.resizeListeners.push(listener);
|
||||
element.addEventListener("resize", listener);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Stop monitoring resize events on an element
|
||||
static clearResizeListeners(element) {
|
||||
while ("resizeListeners" in element)
|
||||
this.removeResizeListener(element, element.resizeListeners[0]);
|
||||
}
|
||||
|
||||
// Produce a custom event object
|
||||
static event(type, component, fields) {
|
||||
let event = new Event(type, {
|
||||
bubbles : true,
|
||||
cancelable: true
|
||||
});
|
||||
if (component)
|
||||
event.component = component;
|
||||
if (fields)
|
||||
Object.assign(event, fields);
|
||||
return event;
|
||||
}
|
||||
|
||||
// Produce a unique element ID
|
||||
static id() {
|
||||
return "i" + (Toolkit.lastId++);
|
||||
return "tk" + (this.nextId++);
|
||||
}
|
||||
|
||||
// Determine whether an object is a component
|
||||
// The user agent may not resolve imports to the same classes
|
||||
static isComponent(o) {
|
||||
return !!this.components.find(c=>o instanceof c);
|
||||
}
|
||||
|
||||
// Determine whether a pointer event is inside an element
|
||||
static isInside(element, e) {
|
||||
let bounds = element.getBoundingClientRect();
|
||||
return (
|
||||
e.offsetX >= 0 && e.offsetX < bounds.width &&
|
||||
e.offsetY >= 0 && e.offsetY < bounds.height
|
||||
);
|
||||
}
|
||||
|
||||
// Generate a list of focusable child elements
|
||||
static listFocusables(element) {
|
||||
return Array.from(element.querySelectorAll(
|
||||
"*:not(*:not(a[href], area, button, details, input, " +
|
||||
"textarea, select, [tabindex='0'])):not([disabled])"
|
||||
)).filter(e=>{
|
||||
for (; e instanceof Element; e = e.parentNode) {
|
||||
let style = getComputedStyle(e);
|
||||
if (style.display == "none" || style.visibility == "hidden")
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Stop monitoring resize events on an element
|
||||
static removeResizeListener(element, listener) {
|
||||
|
||||
// Error checking
|
||||
if (!("resizeListeners" in element))
|
||||
return;
|
||||
let index = element.resizeListeners.indexOf(listener);
|
||||
if (index == -1)
|
||||
return;
|
||||
|
||||
// Remove the listener
|
||||
element.removeEventListener("resize", element.resizeListeners[index]);
|
||||
element.resizeListeners.splice(index, 1);
|
||||
|
||||
// No more listeners: delete the ResizeObserver
|
||||
if (element.resizeListeners.length == 0) {
|
||||
element.resizeObserver.unobserve(element);
|
||||
delete element.resizeListeners;
|
||||
delete element.resizeObserver;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Compute pointer event screen coordinates
|
||||
static screenCoords(e) {
|
||||
return {
|
||||
x: e.screenX / window.devicePixelRatio,
|
||||
y: e.screenY / window.devicePixelRatio
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(options) {
|
||||
super(null, options);
|
||||
|
||||
// Configure instance fields
|
||||
this.locale = Toolkit.NO_LOCALE;
|
||||
this.localized = [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Specify the locale to use for translated strings
|
||||
setLocale(locale) {
|
||||
this.locale = locale || Toolkit.NO_LOCALE;
|
||||
for (let component of this.localized)
|
||||
component.translate();
|
||||
}
|
||||
|
||||
// Translate a string in the selected locale
|
||||
translate(key, component) {
|
||||
|
||||
// Front-end method
|
||||
if (key === undefined) {
|
||||
super.translate();
|
||||
return;
|
||||
}
|
||||
|
||||
// Working variables
|
||||
let subs = component ? component.substitutions : {};
|
||||
key = (key || "").toString().trim();
|
||||
|
||||
// Error checking
|
||||
if (this.locale == null || key == "")
|
||||
return key;
|
||||
|
||||
// Resolve the key first in the substitutions then in the locale
|
||||
let text = key;
|
||||
key = key.toLowerCase();
|
||||
if (key in subs)
|
||||
text = subs[key];
|
||||
else if (key in this.locale)
|
||||
text = this.locale[key];
|
||||
else return "!" + text.toUpperCase();
|
||||
|
||||
// Process all substitutions
|
||||
for (;;) {
|
||||
|
||||
// Working variables
|
||||
let sIndex = 0;
|
||||
let rIndex = -1;
|
||||
let lIndex = -1;
|
||||
let zIndex = -1;
|
||||
|
||||
// Locate the inner-most {} or [] pair
|
||||
for (;;) {
|
||||
let match = Toolkit.subCtrl(text, sIndex);
|
||||
|
||||
// No control characters found
|
||||
if (match == -1)
|
||||
break;
|
||||
sIndex = match + 1;
|
||||
|
||||
// Processing by control character
|
||||
switch (text.charAt(match)) {
|
||||
|
||||
// Opening a substitution group
|
||||
case "{": rIndex = match; continue;
|
||||
case "[": lIndex = match; continue;
|
||||
|
||||
// Closing a recursion group
|
||||
case "}":
|
||||
if (rIndex != -1) {
|
||||
lIndex = -1;
|
||||
zIndex = match;
|
||||
}
|
||||
break;
|
||||
|
||||
// Closing a literal group
|
||||
case "]":
|
||||
if (lIndex != -1) {
|
||||
rIndex = -1;
|
||||
zIndex = match;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Process a recursion substitution
|
||||
if (rIndex != -1) {
|
||||
text =
|
||||
text.substring(0, rIndex) +
|
||||
this.translate(
|
||||
text.substring(rIndex + 1, zIndex),
|
||||
component
|
||||
) +
|
||||
text.substring(zIndex + 1)
|
||||
;
|
||||
}
|
||||
|
||||
// Process a literal substitution
|
||||
else if (lIndex != -1) {
|
||||
text =
|
||||
text.substring(0, lIndex) +
|
||||
text.substring(lIndex + 1, zIndex)
|
||||
.replaceAll("{", "{{")
|
||||
.replaceAll("}", "}}")
|
||||
.replaceAll("[", "[[")
|
||||
.replaceAll("]", "]]")
|
||||
+
|
||||
text.substring(zIndex + 1)
|
||||
;
|
||||
}
|
||||
|
||||
// No more substitutions
|
||||
else break;
|
||||
}
|
||||
|
||||
// Unescape all remaining control characters
|
||||
return (text
|
||||
.replaceAll("{{", "{")
|
||||
.replaceAll("}}", "}")
|
||||
.replaceAll("[[", "[")
|
||||
.replaceAll("]]", "]")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Reduce an object to a single level of depth
|
||||
static flatten(obj, ret = {}, id) {
|
||||
for (let entry of Object.entries(obj)) {
|
||||
let key = (id ? id + "." : "") + entry[0].toLowerCase();
|
||||
let value = entry[1];
|
||||
if (value instanceof Object)
|
||||
this.flatten(value, ret, key);
|
||||
else ret[key] = value;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Register a component for localization management
|
||||
localize(component) {
|
||||
if (this.localized.indexOf(component) != -1)
|
||||
return;
|
||||
this.localized.push(component);
|
||||
component.translate();
|
||||
}
|
||||
|
||||
// Locate a substitution control character in a string
|
||||
static subCtrl(text, index) {
|
||||
for (; index < text.length; index++) {
|
||||
let c = text.charAt(index);
|
||||
if ("{}[]".indexOf(c) == -1)
|
||||
continue;
|
||||
if (index < text.length - 1 || text.charAt(index + 1) != c)
|
||||
return index;
|
||||
index++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
}).initializer();
|
||||
|
||||
|
||||
|
||||
export { Toolkit };
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,667 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// CPU register and disassembler display
|
||||
(globalThis.CPUWindow = class CPUWindow extends Toolkit.Window {
|
||||
|
||||
// Static initializer
|
||||
static initializer() {
|
||||
|
||||
// System register IDs
|
||||
this.ADTRE = 25;
|
||||
this.CHCW = 24;
|
||||
this.ECR = 4;
|
||||
this.EIPC = 0;
|
||||
this.EIPSW = 1;
|
||||
this.FEPC = 2;
|
||||
this.FEPSW = 3;
|
||||
this.PC = -1;
|
||||
this.PIR = 6;
|
||||
this.PSW = 5;
|
||||
this.TKCW = 7;
|
||||
|
||||
// Program register names
|
||||
this.PROGRAM = {
|
||||
[ 2]: "hp",
|
||||
[ 3]: "sp",
|
||||
[ 4]: "gp",
|
||||
[ 5]: "tp",
|
||||
[31]: "lp"
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// Object constructor
|
||||
constructor(debug, options) {
|
||||
super(debug.gui, options);
|
||||
|
||||
// Configure instance fields
|
||||
this.address = 0xFFFFFFF0;
|
||||
this.columns = [ 0, 0, 0, 0 ];
|
||||
this.debug = debug;
|
||||
this.pendingDasm = { mode: null };
|
||||
this.pendingRegs = { mode: null };
|
||||
this.rows = [];
|
||||
|
||||
// Configure properties
|
||||
this.setProperty("sim", "");
|
||||
|
||||
// Configure elements
|
||||
this.initDisassembler();
|
||||
this.initSystemRegisters();
|
||||
this.initProgramRegisters();
|
||||
this.initWindow();
|
||||
|
||||
// Disassembler on the left
|
||||
this.mainWrap.add(this.dasmWrap);
|
||||
|
||||
// Registers on the right
|
||||
this.regs = this.newPanel({
|
||||
layout: "grid",
|
||||
rows : "max-content max-content auto"
|
||||
});
|
||||
this.regs.element.setAttribute("name", "wrap-registers");
|
||||
|
||||
// Splitter between disassembler and registers
|
||||
this.mainSplit = this.newSplitter({
|
||||
component : this.regs,
|
||||
orientation: "vertical",
|
||||
edge : "right",
|
||||
name : "{cpu.mainSplit}"
|
||||
});
|
||||
this.mainSplit.element.setAttribute("name", "split-main");
|
||||
this.mainSplit.element.style.width = "3px";
|
||||
this.mainSplit.element.style.minWidth = "3px";
|
||||
this.mainSplit.element.style.cursor = "ew-resize";
|
||||
this.mainWrap.add(this.mainSplit);
|
||||
|
||||
// Registers on the right
|
||||
this.mainWrap.add(this.regs);
|
||||
|
||||
// System registers on top
|
||||
this.regs.add(this.sysWrap);
|
||||
|
||||
// Splitter between system registers and program registers
|
||||
this.regsSplit = this.regs.add(this.newSplitter({
|
||||
component : this.sysWrap,
|
||||
orientation: "horizontal",
|
||||
edge : "top",
|
||||
name : "{cpu.regsSplit}"
|
||||
}));
|
||||
this.regsSplit.element.style.height = "3px";
|
||||
this.regsSplit.element.style.minHeight = "3px";
|
||||
this.regsSplit.element.style.cursor = "ns-resize";
|
||||
|
||||
// Program registers on the bottom
|
||||
this.regs.add(this.proWrap);
|
||||
}
|
||||
|
||||
// Initialize disassembler pane
|
||||
initDisassembler() {
|
||||
|
||||
// Wrapping element to hide overflowing scrollbar
|
||||
this.dasmWrap = this.newPanel({
|
||||
layout : "grid",
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden"
|
||||
});
|
||||
this.dasmWrap.element.setAttribute("name", "wrap-disassembler");
|
||||
|
||||
// Main element
|
||||
this.dasm = this.dasmWrap.add(this.newPanel({
|
||||
focusable: true,
|
||||
name : "{cpu.disassembler}",
|
||||
overflowX: "auto",
|
||||
overflowY: "hidden"
|
||||
}));
|
||||
this.dasm.element.setAttribute("name", "disassembler");
|
||||
this.dasm.addResizeListener(b=>this.onresize(b));
|
||||
this.dasm.element.addEventListener("keydown", e=>this.onkeydasm(e));
|
||||
this.dasm.element.addEventListener("wheel" , e=>this.onwheel (e));
|
||||
|
||||
this.rows.push(this.dasm.add(new CPUWindow.Row(this.dasm)));
|
||||
}
|
||||
|
||||
// Initialize program registers pane
|
||||
initProgramRegisters() {
|
||||
|
||||
// Wrapping element to hide overflowing scrollbar
|
||||
this.proWrap = this.newPanel({
|
||||
layout : "grid",
|
||||
overflow: "hidden"
|
||||
});
|
||||
this.proWrap.element.setAttribute("name", "wrap-program-registers");
|
||||
|
||||
// Main element
|
||||
this.proRegs = this.proWrap.add(this.newPanel({
|
||||
overflowX: "auto",
|
||||
overflowY: "scroll"
|
||||
}));
|
||||
this.proRegs.element.setAttribute("name", "program-registers");
|
||||
|
||||
// List of registers
|
||||
this.proRegs.registers = {};
|
||||
for (let x = 0; x <= 31; x++)
|
||||
this.addRegister(false, x, CPUWindow.PROGRAM[x] || "r" + x);
|
||||
}
|
||||
|
||||
// Initialize system registers pane
|
||||
initSystemRegisters() {
|
||||
|
||||
// Wrapping element to hide overflowing scrollbar
|
||||
this.sysWrap = this.newPanel({
|
||||
layout : "grid",
|
||||
overflow: "hidden"
|
||||
});
|
||||
this.sysWrap.element.setAttribute("name", "wrap-system-registers");
|
||||
|
||||
// Main element
|
||||
this.sysRegs = this.sysWrap.add(this.newPanel({
|
||||
overflowX: "auto",
|
||||
overflowY: "scroll"
|
||||
}));
|
||||
this.sysRegs.element.setAttribute("name", "system-registers");
|
||||
|
||||
// List of registers
|
||||
this.sysRegs.registers = {};
|
||||
this.addRegister(true, CPUWindow.PC , "PC" );
|
||||
this.addRegister(true, CPUWindow.PSW , "PSW" );
|
||||
this.addRegister(true, CPUWindow.ADTRE, "ADTRE");
|
||||
this.addRegister(true, CPUWindow.CHCW , "CHCW" );
|
||||
this.addRegister(true, CPUWindow.ECR , "ECR" );
|
||||
this.addRegister(true, CPUWindow.EIPC , "EIPC" );
|
||||
this.addRegister(true, CPUWindow.EIPSW, "EIPSW");
|
||||
this.addRegister(true, CPUWindow.FEPC , "FEPC" );
|
||||
this.addRegister(true, CPUWindow.FEPSW, "FEPSW");
|
||||
this.addRegister(true, CPUWindow.PIR , "PIR" );
|
||||
this.addRegister(true, CPUWindow.TKCW , "TKCW" );
|
||||
this.addRegister(true, 29 , "29" );
|
||||
this.addRegister(true, 30 , "30" );
|
||||
this.addRegister(true, 31 , "31" );
|
||||
this.sysRegs.registers[CPUWindow.PSW].setExpanded(true);
|
||||
}
|
||||
|
||||
// Initialize window and client
|
||||
initWindow() {
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("window", "cpu");
|
||||
|
||||
// Configure body
|
||||
this.body.element.setAttribute("filter", "");
|
||||
|
||||
// Configure client
|
||||
this.client.setLayout("grid", {
|
||||
columns: "auto"
|
||||
});
|
||||
|
||||
// Configure main wrapper
|
||||
this.mainWrap = this.client.add(this.newPanel({
|
||||
layout : "grid",
|
||||
columns: "auto max-content max-content"
|
||||
}));
|
||||
this.mainWrap.element.setAttribute("name", "wrap-main");
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Update the display with current emulation data
|
||||
refresh(seekToPC, dasm, regs) {
|
||||
if (dasm || dasm === undefined)
|
||||
this.refreshDasm(this.address, 0, !!seekToPC);
|
||||
if (regs || regs === undefined)
|
||||
this.refreshRegs();
|
||||
}
|
||||
|
||||
// Specify whether the component is visible
|
||||
setVisible(visible, focus) {
|
||||
let prev = this.visible
|
||||
visible = !!visible;
|
||||
super.setVisible(visible, focus);
|
||||
if (visible && !prev)
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Message Methods /////////////////////////////
|
||||
|
||||
// Message received
|
||||
message(msg) {
|
||||
switch (msg.command) {
|
||||
case "GetRegisters": this.getRegisters(msg); break;
|
||||
case "ReadBuffer" : this.readBuffer (msg); break;
|
||||
case "SetRegister" : this.setRegister (msg); break;
|
||||
case "RunNext": case "SingleStep":
|
||||
this.debug.refresh(true); break;
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieved all register values
|
||||
getRegisters(msg) {
|
||||
|
||||
// Update controls
|
||||
this.sysRegs.registers[CPUWindow.PC ]
|
||||
.setValue(msg.pc, msg.pcFrom, msg.pcTo);
|
||||
this.sysRegs.registers[CPUWindow.PSW ].setValue(msg.psw );
|
||||
this.sysRegs.registers[CPUWindow.ADTRE].setValue(msg.adtre);
|
||||
this.sysRegs.registers[CPUWindow.CHCW ].setValue(msg.chcw );
|
||||
this.sysRegs.registers[CPUWindow.ECR ].setValue(msg.ecr );
|
||||
this.sysRegs.registers[CPUWindow.EIPC ].setValue(msg.eipc );
|
||||
this.sysRegs.registers[CPUWindow.EIPSW].setValue(msg.eipsw);
|
||||
this.sysRegs.registers[CPUWindow.FEPC ].setValue(msg.fepc );
|
||||
this.sysRegs.registers[CPUWindow.FEPSW].setValue(msg.fepsw);
|
||||
this.sysRegs.registers[CPUWindow.PIR ].setValue(msg.pir );
|
||||
this.sysRegs.registers[CPUWindow.TKCW ].setValue(msg.tkcw );
|
||||
this.sysRegs.registers[29 ].setValue(msg.sr29 );
|
||||
this.sysRegs.registers[30 ].setValue(msg.sr30 );
|
||||
this.sysRegs.registers[31 ].setValue(msg.sr31 );
|
||||
for (let x = 0; x <= 31; x++)
|
||||
this.proRegs.registers[x].setValue(msg.program[x]);
|
||||
|
||||
// Check for pending display updates
|
||||
let mode = this.pendingDasm.mode;
|
||||
this.pendingRegs.mode = null;
|
||||
switch (mode) {
|
||||
case "first":
|
||||
case null :
|
||||
return;
|
||||
case "refresh":
|
||||
this.refreshRegs();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Retrieved data for disassembly
|
||||
readBuffer(msg) {
|
||||
let lines = Math.min(msg.lines, this.rows.length);
|
||||
|
||||
// Disassemble the visible instructions
|
||||
let dasm = Disassembler.disassemble(
|
||||
new Uint8Array(msg.buffer), 0, msg.address, msg.target,
|
||||
msg.pc, msg.line, lines);
|
||||
|
||||
// Ensure PC is visible if requested
|
||||
let reseeking = false;
|
||||
if (msg.seekToPC) {
|
||||
let visible = this.lines(true);
|
||||
let count = Math.min(msg.lines, visible);
|
||||
let x;
|
||||
|
||||
// Ensure PC is visible in the disassembly
|
||||
for (x = 0; x < count; x++)
|
||||
if (dasm[x].address == msg.pc)
|
||||
break;
|
||||
|
||||
// Seek to display PC in the view
|
||||
if (x == count) {
|
||||
reseeking = true;
|
||||
this.seek(msg.pc, Math.floor(visible / 3));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Not seeking to PC
|
||||
if (!reseeking) {
|
||||
|
||||
// Configure instance fields
|
||||
this.address = dasm[0].address;
|
||||
|
||||
// Configure elements
|
||||
for (let x = 0; x < lines; x++)
|
||||
this.rows[x].update(dasm[x], this.columns, msg.pc);
|
||||
for (let x = 0; x < lines; x++)
|
||||
this.rows[x].setWidths(this.columns);
|
||||
}
|
||||
|
||||
// Check for pending display updates
|
||||
let address = this.pendingDasm.address === null ?
|
||||
this.address : this.pendingDasm.address;
|
||||
let line = this.pendingDasm.line === null ?
|
||||
0 : this.pendingDasm.line ;
|
||||
let mode = this.pendingDasm.mode;
|
||||
this.pendingDasm.mode = null;
|
||||
switch (mode) {
|
||||
case "first":
|
||||
case null :
|
||||
return;
|
||||
case "refresh":
|
||||
case "scroll" :
|
||||
case "seek" :
|
||||
this.refreshDasm(address, line);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Modified a register value
|
||||
setRegister(msg) {
|
||||
(msg.type == "program" ? this.proRegs : this.sysRegs)
|
||||
.registers[msg.id].setValue(msg.value);
|
||||
this.refreshDasm(this.address, 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Insert a register control to a register list
|
||||
addRegister(system, id, name) {
|
||||
let list = system ? this.sysRegs : this.proRegs;
|
||||
let reg = new CPUWindow.Register(this.debug, list, system, id, name);
|
||||
list.registers[id] = reg;
|
||||
list.add(reg);
|
||||
if (reg.expands)
|
||||
list.add(reg.expansion);
|
||||
}
|
||||
|
||||
// The window is being displayed for the first time
|
||||
firstShow() {
|
||||
super.firstShow();
|
||||
this.center();
|
||||
this.mainSplit.measure();
|
||||
this.regsSplit.measure();
|
||||
this.seek(this.address, Math.floor(this.lines(true) / 3));
|
||||
}
|
||||
|
||||
// Determine the height in pixels of one row of output
|
||||
lineHeight() {
|
||||
return Math.max(10, this.rows[0].address.getBounds().height);
|
||||
}
|
||||
|
||||
// Determine the number of rows of output
|
||||
lines(fullyVisible) {
|
||||
let gridHeight = this.dasm.getBounds().height;
|
||||
let lineHeight = this.lineHeight();
|
||||
let ret = gridHeight / lineHeight;
|
||||
ret = fullyVisible ? Math.floor(ret) : Math.ceil(ret);
|
||||
return Math.max(1, ret);
|
||||
}
|
||||
|
||||
// Key down event handler
|
||||
onkeydasm(e) {
|
||||
|
||||
// Control is pressed
|
||||
if (e.ctrlKey) switch (e.key) {
|
||||
|
||||
// Auto-fit
|
||||
case "f": case "F":
|
||||
for (let x = 0; x < this.columns.length; x++)
|
||||
this.columns[x] = 0;
|
||||
for (let row of this.rows)
|
||||
row.setWidths(this.columns);
|
||||
for (let row of this.rows)
|
||||
row.measure(this.columns);
|
||||
for (let row of this.rows)
|
||||
row.setWidths(this.columns);
|
||||
break;
|
||||
|
||||
// Goto
|
||||
case "g": case "G":
|
||||
let addr = prompt(this.application.translate("{app.goto_}"));
|
||||
if (addr === null)
|
||||
break;
|
||||
this.seek(
|
||||
(parseInt(addr, 16) & 0xFFFFFFFE) >>> 0,
|
||||
Math.floor(this.lines(true) / 3)
|
||||
);
|
||||
break;
|
||||
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Processing by key
|
||||
else switch (e.key) {
|
||||
case "ArrowDown": this.scroll( 1); break;
|
||||
case "ArrowUp" : this.scroll(-1); break;
|
||||
case "PageDown" : this.scroll( this.lines(true)); break;
|
||||
case "PageUp" : this.scroll(-this.lines(true)); break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Key down event handler
|
||||
onkeydown(e) {
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
|
||||
// Run next
|
||||
case "F10":
|
||||
this.debug.core.postMessage({
|
||||
command : "RunNext",
|
||||
dbgwnd : "CPU",
|
||||
sim : this.debug.sim,
|
||||
seekToPC: true
|
||||
});
|
||||
break;
|
||||
|
||||
// Single step
|
||||
case "F11":
|
||||
this.debug.core.postMessage({
|
||||
command : "SingleStep",
|
||||
dbgwnd : "CPU",
|
||||
sim : this.debug.sim,
|
||||
seekToPC: true
|
||||
});
|
||||
break;
|
||||
|
||||
default: return super.onkeydown(e);
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Resize event handler
|
||||
onresize(bounds) {
|
||||
|
||||
// Update Splitters
|
||||
this.mainSplit.measure();
|
||||
this.regsSplit.measure();
|
||||
|
||||
// Configure disassembler elements
|
||||
let lines = this.lines(false);
|
||||
for (let y = this.rows.length; y < lines; y++)
|
||||
this.rows[y] = this.dasm.add(new CPUWindow.Row(this.dasm));
|
||||
for (let y = lines; y < this.rows.length; y++)
|
||||
this.dasm.remove(this.rows[y]);
|
||||
if (this.rows.length > lines)
|
||||
this.rows.splice(lines, this.rows.length - lines);
|
||||
this.refreshDasm();
|
||||
}
|
||||
|
||||
// Mouse wheel event handler
|
||||
onwheel(e) {
|
||||
let sign = Math.sign(e.deltaY);
|
||||
let mag = Math.abs (e.deltaY);
|
||||
if (e.deltaMode == WheelEvent.DOM_DELTA_PIXEL)
|
||||
mag = Math.max(1, Math.floor(mag / this.lineHeight()));
|
||||
|
||||
// Configure element
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Configure display
|
||||
this.scroll(sign * mag);
|
||||
}
|
||||
|
||||
// Update the disassembler with current emulation data
|
||||
refreshDasm(address, line, seekToPC) {
|
||||
|
||||
// Do nothing while closed or already waiting to refresh
|
||||
if (!this.isVisible() || this.pendingDasm.mode != null)
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
address = address !== undefined ?
|
||||
(address & 0xFFFFFFFE) >>> 0 : this.address;
|
||||
line = line || 0;
|
||||
let lines = this.lines(false);
|
||||
let start = -10 - Math.max(0, line);
|
||||
let end = lines - Math.min(0, line);
|
||||
|
||||
// Configure pending state
|
||||
this.pendingDasm.mode = "first";
|
||||
this.pendingDasm.address = null;
|
||||
this.pendingDasm.line = null;
|
||||
|
||||
// Request bus data from the WebAssembly core
|
||||
this.debug.core.postMessage({
|
||||
command : "ReadBuffer",
|
||||
sim : this.debug.sim,
|
||||
dbgwnd : "CPU",
|
||||
address : (address + start * 4 & 0xFFFFFFFE) >>> 0,
|
||||
line : line,
|
||||
lines : lines,
|
||||
target : address,
|
||||
seekToPC: !!seekToPC,
|
||||
size : (end - start + 1) * 4
|
||||
});
|
||||
}
|
||||
|
||||
// Update the register list with current emulation data
|
||||
refreshRegs() {
|
||||
|
||||
// Schedule another refresh
|
||||
if (this.pendingRegs.mode != null)
|
||||
this.pendingRegs.mode = "refresh";
|
||||
|
||||
// Do nothing while closed or already waiting to refresh
|
||||
if (!this.isVisible() || this.pendingRegs.mode != null)
|
||||
return;
|
||||
|
||||
// Configure pending state
|
||||
this.pendingRegs.mode = "first";
|
||||
this.pendingRegs.address = null;
|
||||
this.pendingRegs.line = null;
|
||||
|
||||
// Request bus data from the WebAssembly core
|
||||
this.debug.core.postMessage({
|
||||
command: "GetRegisters",
|
||||
dbgwnd : "CPU",
|
||||
sim : this.debug.sim
|
||||
});
|
||||
}
|
||||
|
||||
// Move to a new address relative to the current address
|
||||
scroll(lines) {
|
||||
switch (this.pendingDasm.mode) {
|
||||
case "first" :
|
||||
case "refresh":
|
||||
this.pendingDasm.mode = "scroll";
|
||||
this.pendingDasm.line = -lines;
|
||||
break;
|
||||
case "seek" :
|
||||
case "scroll":
|
||||
this.pendingDasm.mode = "scroll";
|
||||
this.pendingDasm.line -= lines;
|
||||
break;
|
||||
case null:
|
||||
this.refreshDasm(this.address, -lines);
|
||||
}
|
||||
}
|
||||
|
||||
// Move to a new address positioned at a particular row of output
|
||||
seek(address, line) {
|
||||
switch (this.pendingDasm.mode) {
|
||||
case "first" :
|
||||
case "refresh":
|
||||
this.pendingDasm.mode = "seek";
|
||||
this.pendingDasm.address = address;
|
||||
this.pendingDasm.line = line;
|
||||
break;
|
||||
case "seek" :
|
||||
case "scroll":
|
||||
this.pendingDasm.mode = "seek";
|
||||
this.pendingDasm.address = address;
|
||||
this.pendingDasm.line += line;
|
||||
break;
|
||||
case null:
|
||||
this.refreshDasm(address, line);
|
||||
}
|
||||
}
|
||||
|
||||
}).initializer();
|
||||
|
||||
// One row of disassembly
|
||||
CPUWindow.Row = class Row extends Toolkit.Panel {
|
||||
|
||||
// Object constructor
|
||||
constructor(parent) {
|
||||
super(parent.application, {
|
||||
layout : "grid",
|
||||
columns : "repeat(4, max-content)",
|
||||
hollow : false,
|
||||
overflowX: "visible",
|
||||
overflowY: "visible"
|
||||
});
|
||||
|
||||
// Configure element
|
||||
this.element.style.justifyContent = "start";
|
||||
this.element.setAttribute("name", "row");
|
||||
|
||||
// Address column
|
||||
this.address = this.add(parent.newLabel({ text: "\u00a0" }));
|
||||
this.address.element.setAttribute("name", "address");
|
||||
|
||||
// Bytes column
|
||||
this.bytes = this.add(parent.newLabel({ text: "\u00a0" }));
|
||||
this.bytes.element.setAttribute("name", "bytes");
|
||||
|
||||
// Mnemonic column
|
||||
this.mnemonic = this.add(parent.newLabel({ text: "\u00a0" }));
|
||||
this.mnemonic.element.setAttribute("name", "mnemonic");
|
||||
|
||||
// Operands column
|
||||
this.operands = this.add(parent.newLabel({ text: "\u00a0" }));
|
||||
this.operands.element.setAttribute("name", "operands");
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Measure the content widths of each column of output
|
||||
measure(columns) {
|
||||
columns[0] = Math.max(columns[0], this.address .getBounds().width);
|
||||
columns[1] = Math.max(columns[1], this.bytes .getBounds().width);
|
||||
columns[2] = Math.max(columns[2], this.mnemonic.getBounds().width);
|
||||
columns[3] = Math.max(columns[3], this.operands.getBounds().width);
|
||||
}
|
||||
|
||||
// Specify the column widths
|
||||
setWidths(columns) {
|
||||
this.address .element.style.minWidth = columns[0] + "px";
|
||||
this.bytes .element.style.minWidth = columns[1] + "px";
|
||||
this.mnemonic.element.style.minWidth = columns[2] + "px";
|
||||
this.operands.element.style.minWidth = columns[3] + "px";
|
||||
}
|
||||
|
||||
// Update the output labels with emulation state content
|
||||
update(line, columns, pc) {
|
||||
if (pc == line.address)
|
||||
this.element.setAttribute("pc", "");
|
||||
else this.element.removeAttribute("pc");
|
||||
|
||||
this.address.setText(
|
||||
("0000000" + line.address.toString(16).toUpperCase()).slice(-8));
|
||||
|
||||
let bytes = new Array(line.bytes.length);
|
||||
for (let x = 0; x < bytes.length; x++)
|
||||
bytes[x] =
|
||||
("0" + line.bytes[x].toString(16).toUpperCase()).slice(-2);
|
||||
this.bytes.setText(bytes.join(" "));
|
||||
|
||||
this.mnemonic.setText(line.mnemonic);
|
||||
this.operands.setText(line.operands);
|
||||
|
||||
this.measure(columns);
|
||||
}
|
||||
|
||||
};
|
|
@ -1,388 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Decode and format instruction code
|
||||
(globalThis.Disassembler = class Disassembler {
|
||||
|
||||
// Static initializer
|
||||
static initializer() {
|
||||
|
||||
// Bcond conditions
|
||||
this.BCONDS = [
|
||||
"BV" , "BL" , "BZ" , "BNH", "BN", "BR" , "BLT", "BLE",
|
||||
"BNV", "BNL", "BNZ", "BH" , "BP", "NOP", "BGE", "BGT"
|
||||
];
|
||||
|
||||
// Mapping for bit string instruction IDs
|
||||
this.BITSTRING = [
|
||||
"SCH0BSU", "SCH0BSD", "SCH1BSU", "SCH1BSD",
|
||||
null , null , null , null ,
|
||||
"ORBSU" , "ANDBSU" , "XORBSU" , "MOVBSU" ,
|
||||
"ORNBSU" , "ANDNBSU", "XORNBSU", "NOTBSU"
|
||||
];
|
||||
|
||||
// Mapping for floating-point/Nintendo instruction IDs
|
||||
this.FLOATENDO = [
|
||||
"CMPF.S" , null , "CVT.WS" , "CVT.SW" ,
|
||||
"ADDF.S" , "SUBF.S" , "MULF.S" , "DIVF.S" ,
|
||||
"XB" , "XH" , "REV" , "TRNC.SW",
|
||||
"MPYHW"
|
||||
];
|
||||
|
||||
// Opcode definitions
|
||||
this.OPDEFS = [
|
||||
{ format: 1, mnemonic: "MOV" },
|
||||
{ format: 1, mnemonic: "ADD" },
|
||||
{ format: 1, mnemonic: "SUB" },
|
||||
{ format: 1, mnemonic: "CMP" },
|
||||
{ format: 1, mnemonic: "SHL" },
|
||||
{ format: 1, mnemonic: "SHR" },
|
||||
{ format: 1, mnemonic: "JMP" },
|
||||
{ format: 1, mnemonic: "SAR" },
|
||||
{ format: 1, mnemonic: "MUL" },
|
||||
{ format: 1, mnemonic: "DIV" },
|
||||
{ format: 1, mnemonic: "MULU" },
|
||||
{ format: 1, mnemonic: "DIVU" },
|
||||
{ format: 1, mnemonic: "OR" },
|
||||
{ format: 1, mnemonic: "AND" },
|
||||
{ format: 1, mnemonic: "XOR" },
|
||||
{ format: 1, mnemonic: "NOT" },
|
||||
{ format: 2, mnemonic: "MOV" },
|
||||
{ format: 2, mnemonic: "ADD" },
|
||||
{ format: 2, mnemonic: "SETF" },
|
||||
{ format: 2, mnemonic: "CMP" },
|
||||
{ format: 2, mnemonic: "SHL" },
|
||||
{ format: 2, mnemonic: "SHR" },
|
||||
{ format: 2, mnemonic: "CLI" },
|
||||
{ format: 2, mnemonic: "SAR" },
|
||||
{ format: 2, mnemonic: "TRAP" },
|
||||
{ format: 2, mnemonic: "RETI" },
|
||||
{ format: 2, mnemonic: "HALT" },
|
||||
{ format: 0, mnemonic: null },
|
||||
{ format: 2, mnemonic: "LDSR" },
|
||||
{ format: 2, mnemonic: "STSR" },
|
||||
{ format: 2, mnemonic: "SEI" },
|
||||
{ format: 2, mnemonic: null },
|
||||
{ format: 3, mnemonic: null },
|
||||
{ format: 3, mnemonic: null },
|
||||
{ format: 3, mnemonic: null },
|
||||
{ format: 3, mnemonic: null },
|
||||
{ format: 3, mnemonic: null },
|
||||
{ format: 3, mnemonic: null },
|
||||
{ format: 3, mnemonic: null },
|
||||
{ format: 3, mnemonic: null },
|
||||
{ format: 5, mnemonic: "MOVEA" },
|
||||
{ format: 5, mnemonic: "ADDI" },
|
||||
{ format: 4, mnemonic: "JR" },
|
||||
{ format: 4, mnemonic: "JAL" },
|
||||
{ format: 5, mnemonic: "ORI" },
|
||||
{ format: 5, mnemonic: "ANDI" },
|
||||
{ format: 5, mnemonic: "XORI" },
|
||||
{ format: 5, mnemonic: "MOVHI" },
|
||||
{ format: 6, mnemonic: "LD.B" },
|
||||
{ format: 6, mnemonic: "LD.H" },
|
||||
{ format: 0, mnemonic: null },
|
||||
{ format: 6, mnemonic: "LD.W" },
|
||||
{ format: 6, mnemonic: "ST.B" },
|
||||
{ format: 6, mnemonic: "ST.H" },
|
||||
{ format: 0, mnemonic: null },
|
||||
{ format: 6, mnemonic: "ST.W" },
|
||||
{ format: 6, mnemonic: "IN.B" },
|
||||
{ format: 6, mnemonic: "IN.H" },
|
||||
{ format: 6, mnemonic: "CAXI" },
|
||||
{ format: 6, mnemonic: "IN.W" },
|
||||
{ format: 6, mnemonic: "OUT.B" },
|
||||
{ format: 6, mnemonic: "OUT.H" },
|
||||
{ format: 7, mnemonic: null },
|
||||
{ format: 6, mnemonic: "OUT.W" }
|
||||
];
|
||||
|
||||
// Program register names
|
||||
this.PROREGNAMES = [
|
||||
"r0" , "r1" , "hp" , "sp" , "gp" , "tp" , "r6" , "r7" ,
|
||||
"r8" , "r9" , "r10", "r11", "r12", "r13", "r14", "r15",
|
||||
"r16", "r17", "r18", "r19", "r20", "r21", "r22", "r23",
|
||||
"r24", "r25", "r26", "r27", "r28", "r29", "r30", "lp"
|
||||
];
|
||||
|
||||
// SETF conditions
|
||||
this.SETFS = [
|
||||
"V" , "L" , "Z" , "NH", "N", "T", "LT", "LE",
|
||||
"NV", "NL", "NZ", "H" , "P", "F", "GE", "GT"
|
||||
];
|
||||
|
||||
// System register names
|
||||
this.SYSREGNAMES = [
|
||||
"EIPC", "EIPSW", "FEPC", "FEPSW", "ECR", "PSW", "PIR", "TKCW",
|
||||
"8" , "9" , "10" , "11" , "12" , "13" , "14" , "15" ,
|
||||
"16" , "17" , "18" , "19" , "20" , "21" , "22" , "23" ,
|
||||
"CHCW", "ADTRE", "26" , "27" , "28" , "29" , "30" , "31"
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**************************** Static Methods *****************************/
|
||||
|
||||
// Disassemble instructions as lines of text
|
||||
static disassemble(buffer, doffset, address, target, pc, line, lines) {
|
||||
let history, hIndex;
|
||||
|
||||
// Two bytes before PC to ensure PC isn't skipped
|
||||
let prePC = (pc - 2 & 0xFFFFFFFF) >>> 0;
|
||||
|
||||
// Prepare history buffer
|
||||
if (line > 0) {
|
||||
history = new Array(line);
|
||||
hIndex = 0;
|
||||
}
|
||||
|
||||
// Locate the line containing the target address
|
||||
for (;;) {
|
||||
|
||||
// Emergency error checking
|
||||
if (doffset >= buffer.length)
|
||||
throw "Error: Target address not in disassembly buffer";
|
||||
|
||||
// Determine the size of the current line of output
|
||||
let size = address == prePC ||
|
||||
this.OPDEFS[buffer[doffset + 1] >>> 2].format < 4 ? 2 : 4;
|
||||
|
||||
// The line contians the target address
|
||||
if ((target - address & 0xFFFFFFFF) >>> 0 < size)
|
||||
break;
|
||||
|
||||
// Record the current line in the history
|
||||
if (line > 0) {
|
||||
let item = history[hIndex] = history[hIndex] || {};
|
||||
hIndex = hIndex < history.length - 1 ? hIndex + 1 : 0;
|
||||
item.address = address;
|
||||
item.doffset = doffset;
|
||||
}
|
||||
|
||||
// Advance to the next line
|
||||
doffset += size;
|
||||
address = (address + size & 0xFFFFFFFF) >>> 0;
|
||||
}
|
||||
|
||||
// The target address is before the first line of output
|
||||
for (; line < 0; line++) {
|
||||
let size = address == prePC ||
|
||||
this.OPDEFS[buffer[doffset + 1] >>> 2].format < 4 ? 2 : 4;
|
||||
doffset += size;
|
||||
address = (address + size & 0xFFFFFFFF) >>> 0;
|
||||
}
|
||||
|
||||
// The target address is after the first line of output
|
||||
if (line > 0) {
|
||||
let item = history[hIndex];
|
||||
|
||||
// Emergency error checking
|
||||
if (!item)
|
||||
throw "Error: First output not in disassembly history";
|
||||
|
||||
// Inherit the address of the first history item
|
||||
address = item.address;
|
||||
doffset = item.doffset;
|
||||
}
|
||||
|
||||
// Decode the lines of the output
|
||||
let ret = new Array(lines);
|
||||
for (let x = 0; x < lines; x++) {
|
||||
let inst = ret[x] = this.decode(buffer, doffset, address);
|
||||
let size = address == prePC ? 2 : inst.size;
|
||||
doffset += size;
|
||||
address = (address + size & 0xFFFFFFFF) >>> 0;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**************************** Private Methods ****************************/
|
||||
|
||||
// Retrieve the bits for an instruction
|
||||
static bits(inst) {
|
||||
return inst.size == 2 ?
|
||||
inst.bytes[1] << 8 | inst.bytes[0] : (
|
||||
inst.bytes[1] << 24 | inst.bytes[0] << 16 |
|
||||
inst.bytes[3] << 8 | inst.bytes[2]
|
||||
) >>> 0
|
||||
;
|
||||
}
|
||||
|
||||
// Decode one line of output
|
||||
static decode(buffer, doffset, address) {
|
||||
let opcode = buffer[doffset + 1] >>> 2;
|
||||
let opdef = this.OPDEFS[opcode];
|
||||
let size = opdef.format < 4 ? 2 : 4;
|
||||
|
||||
// Emergency error checking
|
||||
if (doffset + size > buffer.length)
|
||||
throw "Error: Insufficient disassembly data";
|
||||
|
||||
// Produce output line object
|
||||
let inst = {
|
||||
address : address,
|
||||
mnemonic: opdef.mnemonic,
|
||||
opcode : opcode,
|
||||
operands: null,
|
||||
size : size,
|
||||
bytes : new Uint8Array(
|
||||
buffer.buffer.slice(doffset, doffset + size))
|
||||
};
|
||||
|
||||
// Processing by instruction format
|
||||
switch (opdef.format) {
|
||||
case 1: this.decodeFormat1(inst ); break;
|
||||
case 2: this.decodeFormat2(inst ); break;
|
||||
case 3: this.decodeFormat3(inst, address); break;
|
||||
case 4: this.decodeFormat4(inst, address); break;
|
||||
case 5: this.decodeFormat5(inst ); break;
|
||||
case 6: this.decodeFormat6(inst ); break;
|
||||
case 7: this.decodeFormat7(inst ); break;
|
||||
}
|
||||
|
||||
// Illegal opcode
|
||||
if (inst.mnemonic == null)
|
||||
inst.mnemonic = "---";
|
||||
|
||||
return inst;
|
||||
}
|
||||
|
||||
// Format I
|
||||
static decodeFormat1(inst) {
|
||||
let bits = this.bits(inst);
|
||||
let reg1 = this.PROREGNAMES[bits & 31];
|
||||
switch (inst.opcode) {
|
||||
case 0b000110: // JMP
|
||||
inst.operands = "[" + reg1 + "]";
|
||||
break;
|
||||
default: // All others
|
||||
inst.operands = reg1 + ", " + this.PROREGNAMES[bits >> 5 & 31];
|
||||
}
|
||||
}
|
||||
|
||||
// Format II
|
||||
static decodeFormat2(inst) {
|
||||
let bits = this.bits(inst);
|
||||
let reg2 = this.PROREGNAMES[bits >> 5 & 31];
|
||||
let other = bits & 31;
|
||||
switch (inst.opcode) {
|
||||
case 0b010010: // SETF
|
||||
inst.operands = this.SETFS[other & 15] + ", " + reg2;
|
||||
break;
|
||||
case 0b011000: // TRAP
|
||||
inst.operands = other;
|
||||
break;
|
||||
case 0b011100: // LDSR
|
||||
inst.operands = reg2 + ", " + this.SYSREGNAMES[other];
|
||||
break;
|
||||
case 0b011101: // STSR
|
||||
inst.operands = this.SYSREGNAMES[other] + ", " + reg2;
|
||||
break;
|
||||
case 0b011111: // Bit string
|
||||
inst.mnemonic = this.BITSTRING[other];
|
||||
break;
|
||||
case 0b010110: // CLI
|
||||
case 0b011001: // RETI
|
||||
case 0b011010: // HALT
|
||||
case 0b011110: // SEI
|
||||
break;
|
||||
case 0b010000: // MOV
|
||||
case 0b010001: // ADD
|
||||
case 0b010011: // CMP
|
||||
inst.operands = this.signExtend(other, 5) + ", " + reg2;
|
||||
break;
|
||||
default: // SHL, SHR, SAR
|
||||
inst.operands = other + ", " + reg2;
|
||||
}
|
||||
}
|
||||
|
||||
// Format III
|
||||
static decodeFormat3(inst, address) {
|
||||
let bits = this.bits(inst);
|
||||
let disp = this.signExtend(bits & 0x1FF, 9);
|
||||
let cond = bits >> 9 & 15;
|
||||
inst.mnemonic = this.BCONDS[cond];
|
||||
if (cond == 13)
|
||||
return; // NOP
|
||||
inst.operands = ("0000000" + ((address + disp & 0xFFFFFFFF) >>> 0)
|
||||
.toString(16).toUpperCase()).slice(-8);
|
||||
}
|
||||
|
||||
// Format IV
|
||||
static decodeFormat4(inst, address) {
|
||||
let bits = this.bits(inst);
|
||||
let disp = this.signExtend(bits & 0x3FFFFFF, 26);
|
||||
inst.operands = ("0000000" + ((address + disp & 0xFFFFFFFF) >>> 0)
|
||||
.toString(16).toUpperCase()).slice(-8);
|
||||
}
|
||||
|
||||
// Format V
|
||||
static decodeFormat5(inst) {
|
||||
let bits = this.bits(inst);
|
||||
let reg2 = this.PROREGNAMES[bits >> 21 & 31];
|
||||
let reg1 = this.PROREGNAMES[bits >> 16 & 31];
|
||||
let imm = bits & 0xFFFF;
|
||||
switch (inst.opcode) {
|
||||
case 0b101001: // ADDI
|
||||
inst.operands =
|
||||
this.signExtend(imm, 16) + ", " + reg1 + ", " + reg2;
|
||||
break;
|
||||
default: // All others
|
||||
inst.operands = "0x" + ("000" + imm.toString(16).toUpperCase())
|
||||
.slice(-4) + ", " + reg1 + ", " + reg2;
|
||||
}
|
||||
}
|
||||
|
||||
// Format VI
|
||||
static decodeFormat6(inst) {
|
||||
let bits = this.bits(inst);
|
||||
let reg2 = this.PROREGNAMES[bits >> 21 & 31];
|
||||
let reg1 = this.PROREGNAMES[bits >> 16 & 31];
|
||||
let disp = this.signExtend(bits & 0xFFFF, 16);
|
||||
disp = disp == 0 ? "" : (disp < -255 || disp > 255) ? "0x" +
|
||||
("000" + (disp & 0xFFFF).toString(16).toUpperCase()).slice(-4) :
|
||||
(disp < 0 ? "-" : "") + "0x" +
|
||||
Math.abs(disp).toString(16).toUpperCase();
|
||||
switch (inst.opcode) {
|
||||
case 0b110000: // LD.B
|
||||
case 0b110001: // LD.H
|
||||
case 0b110011: // LD.W
|
||||
case 0b111000: // IN.B
|
||||
case 0b111001: // IN.H
|
||||
case 0b111010: // CAXI
|
||||
case 0b111011: // IN.W
|
||||
inst.operands = disp + "[" + reg1 + "], " + reg2;
|
||||
break;
|
||||
default: // Output and store
|
||||
inst.operands = reg2 + ", " + disp + "[" + reg1 + "]";
|
||||
}
|
||||
}
|
||||
|
||||
// Format VII
|
||||
static decodeFormat7(inst) {
|
||||
let bits = this.bits(inst);
|
||||
let reg2 = this.PROREGNAMES[bits >> 21 & 31];
|
||||
let reg1 = this.PROREGNAMES[bits >> 16 & 31];
|
||||
let subop = bits >> 10 & 63;
|
||||
inst.mnemonic = this.FLOATENDO[subop];
|
||||
if (inst.mnemonic == null)
|
||||
return;
|
||||
switch (subop) {
|
||||
case 0b001000: // XB
|
||||
case 0b001001: // XH
|
||||
inst.operands = reg2;
|
||||
break;
|
||||
default: // All others
|
||||
inst.operands = reg1 + ", " + reg2;
|
||||
}
|
||||
}
|
||||
|
||||
// Sign extend a value
|
||||
static signExtend(value, bits) {
|
||||
return value & 1 << bits - 1 ? value | -1 << bits : value;
|
||||
}
|
||||
|
||||
}).initializer();
|
|
@ -1,502 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Hex editor style memory viewer
|
||||
globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window {
|
||||
|
||||
// Object constructor
|
||||
constructor(debug, options) {
|
||||
super(debug.gui, options);
|
||||
|
||||
// Configure instance fields
|
||||
this.address = 0x05000000;
|
||||
this.debug = debug;
|
||||
this.editDigit = null;
|
||||
this.pending = { mode: null };
|
||||
this.rows = [];
|
||||
this.selected = this.address;
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("window", "memory");
|
||||
|
||||
// Configure body
|
||||
this.body.element.setAttribute("filter", "");
|
||||
|
||||
// Configure client
|
||||
this.client.setLayout("grid");
|
||||
|
||||
// Wrapping element to hide overflowing scrollbar
|
||||
this.hexWrap = this.client.add(this.newPanel({
|
||||
layout : "grid",
|
||||
columns: "auto"
|
||||
}));
|
||||
this.hexWrap.element.setAttribute("name", "wrap-hex");
|
||||
|
||||
// Configure hex viewer
|
||||
this.hex = this.hexWrap.add(this.client.newPanel({
|
||||
focusable: true,
|
||||
layout : "block",
|
||||
hollow : false,
|
||||
name : "{memory.hexEditor}",
|
||||
overflowX: "auto",
|
||||
overflowY: "hidden"
|
||||
}));
|
||||
this.hex.element.setAttribute("role", "grid");
|
||||
this.hex.element.setAttribute("name", "hex");
|
||||
this.hex.element.addEventListener("keydown", e=>this.onkeyhex(e));
|
||||
this.hex.element.addEventListener("wheel" , e=>this.onwheel (e));
|
||||
this.hex.addResizeListener(b=>this.onresize());
|
||||
|
||||
// Configure properties
|
||||
this.setProperty("sim", "");
|
||||
this.rows.push(this.hex.add(new MemoryWindow.Row(this, this.hex, 0)));
|
||||
this.application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Update the display with current emulation data
|
||||
refresh(address) {
|
||||
|
||||
// Do nothing while closed or already waiting to refresh
|
||||
if (!this.isVisible() || this.pending.mode !== null)
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
address = address === undefined ? this.address : address;
|
||||
let lines = this.lines(false);
|
||||
|
||||
// Configure pending state
|
||||
this.pending.mode = "first";
|
||||
this.pending.address = null;
|
||||
this.pending.line = null;
|
||||
|
||||
// Request bus data from the WebAssembly core
|
||||
this.debug.core.postMessage({
|
||||
command: "ReadBuffer",
|
||||
sim : this.debug.sim,
|
||||
dbgwnd : "Memory",
|
||||
address: address,
|
||||
lines : lines,
|
||||
size : lines * 16
|
||||
});
|
||||
}
|
||||
|
||||
// Specify whether the component is visible
|
||||
setVisible(visible, focus) {
|
||||
let prev = this.visible
|
||||
visible = !!visible;
|
||||
super.setVisible(visible, focus);
|
||||
if (visible && !prev)
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Message Methods /////////////////////////////
|
||||
|
||||
// Message received
|
||||
message(msg) {
|
||||
switch (msg.command) {
|
||||
case "ReadBuffer": this.readBuffer(msg) ; break;
|
||||
case "Write" : this.debug.refresh(false); break;
|
||||
}
|
||||
}
|
||||
|
||||
// Received bytes from the bus
|
||||
readBuffer(msg) {
|
||||
let buffer = new Uint8Array(msg.buffer);
|
||||
let lines = Math.min(msg.lines, this.rows.length);
|
||||
|
||||
// Configure instance fields
|
||||
this.address = msg.address;
|
||||
|
||||
// Update display
|
||||
for (
|
||||
let x = 0, address = msg.address, offset = 0;
|
||||
x < lines && offset < buffer.length;
|
||||
x++, address = (address + 16 & 0xFFFFFFF0) >>> 0, offset += 16
|
||||
) this.rows[x].update(buffer, offset);
|
||||
|
||||
// Check for pending display updates
|
||||
let address = this.pending.address === null ?
|
||||
this.address : this.pending.address;
|
||||
let line = this.pending.line === null ?
|
||||
0 : this.pending.line ;
|
||||
let mode = this.pending.mode;
|
||||
this.pending.mode = null;
|
||||
switch (mode) {
|
||||
case "first":
|
||||
case null :
|
||||
return;
|
||||
case "refresh":
|
||||
case "scroll" :
|
||||
case "seek" :
|
||||
this.refresh((address + line * 16 & 0xFFFFFFF0) >>> 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Write the current edited value to the bus
|
||||
commit(value) {
|
||||
this.editDigit = null;
|
||||
this.debug.core.postMessage({
|
||||
command: "Write",
|
||||
sim : this.debug.sim,
|
||||
dbgwnd : "Memory",
|
||||
debug : true,
|
||||
address: this.selected,
|
||||
type : 0,
|
||||
value : value
|
||||
});
|
||||
}
|
||||
|
||||
// The window is being displayed for the first time
|
||||
firstShow() {
|
||||
super.firstShow();
|
||||
this.center();
|
||||
}
|
||||
|
||||
// Determine the height in pixels of one row of output
|
||||
lineHeight() {
|
||||
return Math.max(10, this.rows[0].addr.getBounds().height);
|
||||
}
|
||||
|
||||
// Determine the number of rows of output
|
||||
lines(fullyVisible) {
|
||||
let gridHeight = this.hex.getBounds().height;
|
||||
let lineHeight = this.lineHeight();
|
||||
let ret = gridHeight / lineHeight;
|
||||
ret = fullyVisible ? Math.floor(ret) : Math.ceil(ret);
|
||||
return Math.max(1, ret);
|
||||
}
|
||||
|
||||
// Focus lost event capture
|
||||
onblur(e) {
|
||||
super.onblur(e);
|
||||
if (this.editDigit !== null && !this.contains(e.relatedTarget))
|
||||
this.commit(this.editDigit);
|
||||
}
|
||||
|
||||
// Key down event handler
|
||||
onkeydown(e) {
|
||||
let change = null;
|
||||
let digit = null;
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
case "ArrowDown" : change = 16 ; break;
|
||||
case "ArrowLeft" : change = - 1 ; break;
|
||||
case "ArrowRight": change = 1 ; break;
|
||||
case "ArrowUp" : change = -16 ; break;
|
||||
case "Enter" : change = 0 ; break;
|
||||
case "PageDown" : change = visible; break;
|
||||
case "PageUp" : change = -visible; break;
|
||||
case "0": case "1": case "2": case "3": case "4":
|
||||
case "5": case "6": case "7": case "8": case "9":
|
||||
digit = e.key.codePointAt(0) - "0".codePointAt(0);
|
||||
break;
|
||||
case "a": case "b": case "c": case "d": case "e": case "f":
|
||||
digit = e.key.codePointAt(0) - "a".codePointAt(0) + 10;
|
||||
break;
|
||||
case "A": case "B": case "C": case "D": case "E": case "F":
|
||||
digit = e.key.codePointAt(0) - "A".codePointAt(0) + 10;
|
||||
break;
|
||||
default: return super.onkeydown(e);
|
||||
}
|
||||
|
||||
// Moving the selection
|
||||
if (change !== null) {
|
||||
if (this.editDigit !== null)
|
||||
this.commit(this.editDigit);
|
||||
this.setSelected((this.selected + change & 0xFFFFFFFF) >>> 0);
|
||||
}
|
||||
|
||||
// Entering a digit
|
||||
if (digit !== null) {
|
||||
let selected = this.selected;
|
||||
if (this.editDigit !== null) {
|
||||
this.commit(this.editDigit << 4 | digit);
|
||||
selected++;
|
||||
} else this.editDigit = digit;
|
||||
if (!this.setSelected(selected))
|
||||
for (let row of this.rows)
|
||||
row.update();
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Key down event handler
|
||||
onkeyhex(e) {
|
||||
|
||||
// Control is not pressed
|
||||
if (!e.ctrlKey)
|
||||
return;
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
case "g": case "G":
|
||||
let addr = prompt(this.application.translate("{app.goto_}"));
|
||||
if (addr === null)
|
||||
break;
|
||||
this.setSelected((parseInt(addr, 16) & 0xFFFFFFFF) >>> 0);
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Resize event handler
|
||||
onresize() {
|
||||
let lines = this.lines(false);
|
||||
for (let y = this.rows.length; y < lines; y++)
|
||||
this.rows[y] =
|
||||
this.hex.add(new MemoryWindow.Row(this, this.hex, y * 16));
|
||||
for (let y = lines; y < this.rows.length; y++)
|
||||
this.hex.remove(this.rows[y]);
|
||||
if (this.rows.length > lines)
|
||||
this.rows.splice(lines, this.rows.length - lines);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
// Mouse wheel event handler
|
||||
onwheel(e) {
|
||||
let sign = Math.sign(e.deltaY);
|
||||
let mag = Math.abs (e.deltaY);
|
||||
if (e.deltaMode == WheelEvent.DOM_DELTA_PIXEL)
|
||||
mag = Math.max(1, Math.floor(mag / this.lineHeight()));
|
||||
|
||||
// Configure element
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Configure display
|
||||
this.scroll(sign * mag);
|
||||
}
|
||||
|
||||
// Move to a new address relative to the current address
|
||||
scroll(lines) {
|
||||
switch (this.pending.mode) {
|
||||
case "first" :
|
||||
case "refresh":
|
||||
this.pending.mode = "scroll";
|
||||
this.pending.line = lines;
|
||||
break;
|
||||
case "scroll":
|
||||
case "seek" :
|
||||
this.pending.mode = "scroll";
|
||||
this.pending.line += lines;
|
||||
break;
|
||||
case null:
|
||||
this.refresh((this.address + lines * 16 & 0xFFFFFFF0) >>> 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Move to a new address positioned at a particular row of output
|
||||
seek(address, line) {
|
||||
switch (this.pending.mode) {
|
||||
case "first" :
|
||||
case "refresh":
|
||||
this.pending.mode = "seek";
|
||||
this.pending.address = address;
|
||||
this.pending.line = line;
|
||||
break;
|
||||
case "scroll":
|
||||
case "seek" :
|
||||
this.pending.mode = "seek";
|
||||
this.pending.address = address;
|
||||
this.pending.line += line;
|
||||
break;
|
||||
case null:
|
||||
this.refresh((address - line * 16 & 0xFFFFFFF0) >>> 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Specify which byte value is selected
|
||||
setSelected(selected) {
|
||||
|
||||
// The selected cell is not changing
|
||||
if (selected == this.selected)
|
||||
return false;
|
||||
|
||||
// An edit was in progress
|
||||
if (this.editDigit !== null)
|
||||
this.commit(this.editDigit);
|
||||
|
||||
// Working variables
|
||||
let pos = (selected - this.address & 0xFFFFFFFF) >>> 0;
|
||||
let visible = this.lines(true) * 16;
|
||||
|
||||
// The selected cell is visible
|
||||
if (pos >= 0 && pos < visible) {
|
||||
this.selected = selected;
|
||||
for (let y = 0; y < this.rows.length; y++)
|
||||
this.rows[y].checkSelected();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Working variables
|
||||
let down = (selected - this.address & 0xFFFFFFF0) >>> 0;
|
||||
let up = (this.address - selected + 15 & 0xFFFFFFF0) >>> 0;
|
||||
|
||||
// Seek to show the new selection in the view
|
||||
this.selected = selected;
|
||||
if (down <= up) {
|
||||
this.seek((this.address + down & 0xFFFFFFFF) >>> 0,
|
||||
visible / 16 - 1);
|
||||
} else this.seek((this.address - up & 0xFFFFFFFF) >>> 0, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// One row of output
|
||||
MemoryWindow.Row = class Row extends Toolkit.Panel {
|
||||
|
||||
// Object constructor
|
||||
constructor(wnd, parent, offset) {
|
||||
super(parent.application, {
|
||||
layout : "grid",
|
||||
columns : "repeat(17, max-content)",
|
||||
hollow : false,
|
||||
overflowX: "visible",
|
||||
overflowY: "visible"
|
||||
});
|
||||
|
||||
// Configure instance fields
|
||||
this.cells = new Array(16);
|
||||
this.offset = offset;
|
||||
this.wnd = wnd;
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("role", "row");
|
||||
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
||||
|
||||
// Address label
|
||||
this.addr = this.add(parent.newLabel({ text: "\u00a0" }));
|
||||
this.addr.element.setAttribute("role", "gridcell");
|
||||
this.addr.element.setAttribute("name", "address");
|
||||
|
||||
// Byte labels
|
||||
for (let x = 0; x < 16; x++)
|
||||
this.cells[x] = new MemoryWindow.Cell(wnd, this, offset + x);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Check whether any byte label is the selected byte
|
||||
checkSelected() {
|
||||
for (let cell of this.cells)
|
||||
cell.checkSelected();
|
||||
}
|
||||
|
||||
// Update the output labels with emulation state content
|
||||
update(bytes, offset) {
|
||||
this.addr.setText(
|
||||
("0000000" + this.address().toString(16).toUpperCase()).slice(-8));
|
||||
for (let cell of this.cells)
|
||||
cell.update(bytes ? bytes[offset++] : cell.value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Compute the current address of the row
|
||||
address() {
|
||||
return (this.wnd.address + this.offset & 0xFFFFFFFF) >>> 0;
|
||||
}
|
||||
|
||||
// Pointer down event handler
|
||||
onpointerdown(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0)
|
||||
return;
|
||||
|
||||
// Check whether the click is within the columns of cells
|
||||
let next = this.cells[0].getBounds();
|
||||
if (e.x < next.left)
|
||||
return;
|
||||
|
||||
// Compute which cell was clicked
|
||||
for (let x = 0; x < 16; x++) {
|
||||
let cur = next;
|
||||
if (x < 15) {
|
||||
next = this.cells[x + 1].getBounds();
|
||||
if (e.x < (cur.right + next.left) / 2)
|
||||
return this.wnd.setSelected(this.cells[x].address());
|
||||
} else if (e.x < cur.right)
|
||||
return this.wnd.setSelected(this.cells[x].address());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// One cell of output
|
||||
MemoryWindow.Cell = class Cell extends Toolkit.Label {
|
||||
|
||||
// Object constructor
|
||||
constructor(wnd, parent, offset) {
|
||||
super(wnd.application, { text: "\u00a0" });
|
||||
|
||||
// Configure instance fields
|
||||
this.offset = offset;
|
||||
this.wnd = wnd;
|
||||
this.value = 0x00;
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("role", "gridcell");
|
||||
this.element.setAttribute("name", "byte");
|
||||
|
||||
parent.add(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Check whether this cell is the selected cell
|
||||
checkSelected() {
|
||||
let selected = this.address() == this.wnd.selected;
|
||||
if (selected)
|
||||
this.element.setAttribute("selected", "");
|
||||
else this.element.removeAttribute("selected");
|
||||
return selected;
|
||||
}
|
||||
|
||||
// Update the output with emulation state content
|
||||
update(value) {
|
||||
if (value === undefined)
|
||||
value = this.value;
|
||||
else this.value = value;
|
||||
if (this.checkSelected() && this.wnd.editDigit !== null) {
|
||||
this.setText("\u00a0" +
|
||||
this.wnd.editDigit.toString(16).toUpperCase());
|
||||
} else this.setText(("0"+value.toString(16).toUpperCase()).slice(-2));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Compute the current address of the cell
|
||||
address() {
|
||||
return (this.wnd.address + this.offset & 0xFFFFFFFF) >>> 0;
|
||||
}
|
||||
|
||||
};
|
|
@ -1,520 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// List item for CPU window register lists
|
||||
(CPUWindow.Register = class Register extends Toolkit.Panel {
|
||||
|
||||
// Static initializer
|
||||
static initializer() {
|
||||
let buffer = new ArrayBuffer(4);
|
||||
this.F32 = new Float32Array(buffer);
|
||||
this.S32 = new Int32Array (buffer);
|
||||
this.U32 = new Uint32Array (buffer);
|
||||
}
|
||||
|
||||
// Object constructor
|
||||
constructor(debug, parent, system, id, name) {
|
||||
super(parent.application, {
|
||||
layout : "grid",
|
||||
columns : "auto max-content",
|
||||
overflowX: "visible",
|
||||
overflowY: "visible"
|
||||
});
|
||||
|
||||
// Configure instance fields
|
||||
this.debug = debug;
|
||||
this.expanded = false;
|
||||
this.expansion = null;
|
||||
this.fields = {};
|
||||
this.format = "hex";
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.parent = parent;
|
||||
this.system = system;
|
||||
this.value = 0x00000000;
|
||||
|
||||
// Determine whether the register has expansion fields
|
||||
this.expands = !system;
|
||||
switch (id) {
|
||||
case CPUWindow.CHCW:
|
||||
case CPUWindow.ECR:
|
||||
case CPUWindow.EIPSW:
|
||||
case CPUWindow.FEPSW:
|
||||
case CPUWindow.PC:
|
||||
case CPUWindow.PIR:
|
||||
case CPUWindow.PSW:
|
||||
case CPUWindow.TKCW:
|
||||
this.expands = true;
|
||||
}
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("name", "register");
|
||||
this.element.setAttribute("format", this.format);
|
||||
|
||||
// Name/expansion "check box"
|
||||
this.chkExpand = this.add(this.newCheckBox({
|
||||
enabled: this.expands,
|
||||
text : name
|
||||
}));
|
||||
this.chkExpand.element.setAttribute("name", "expand");
|
||||
this.chkExpand.element.setAttribute("aria-expanded", "false");
|
||||
this.chkExpand.addChangeListener(e=>
|
||||
this.setExpanded(this.chkExpand.isChecked()));
|
||||
|
||||
// Value text box
|
||||
this.txtValue = this.add(this.newTextBox({ text: "\u00a0" }));
|
||||
this.txtValue.element.setAttribute("name", "value");
|
||||
this.txtValue.addCommitListener(e=>this.onvalue());
|
||||
|
||||
// Expansion controls
|
||||
if (!system)
|
||||
this.expansionProgram();
|
||||
else switch (id) {
|
||||
case CPUWindow.CHCW : this.expansionCHCW(); break;
|
||||
case CPUWindow.ECR : this.expansionECR (); break;
|
||||
case CPUWindow.EIPSW:
|
||||
case CPUWindow.FEPSW:
|
||||
case CPUWindow.PSW : this.expansionPSW (); break;
|
||||
case CPUWindow.PC : this.expansionPC (); break;
|
||||
case CPUWindow.PIR : this.expansionPIR (); break;
|
||||
case CPUWindow.TKCW : this.expansionTKCW(); break;
|
||||
}
|
||||
if (this.expands) {
|
||||
this.chkExpand.element.setAttribute(
|
||||
"aria-controls", this.expansion.id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Static Methods //////////////////////////////
|
||||
|
||||
// Force a 32-bit integer to be signed
|
||||
static asSigned(value) {
|
||||
this.U32[0] = value >>> 0;
|
||||
return this.S32[0];
|
||||
}
|
||||
|
||||
// Interpret a 32-bit integer as a float
|
||||
static intBitsToFloat(value) {
|
||||
this.U32[0] = value;
|
||||
value = this.F32[0];
|
||||
return Number.isFinite(value) ? value : 0;
|
||||
}
|
||||
|
||||
// Interpret a float as a 32-bit integer
|
||||
static floatToIntBits(value) {
|
||||
if (!Number.isFinite(value))
|
||||
return 0;
|
||||
this.F32[0] = value;
|
||||
return this.U32[0];
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Specify whether the expansion fields are visible
|
||||
setExpanded(expanded) {
|
||||
this.expanded = expanded = !!expanded;
|
||||
this.expansion.setVisible(expanded);
|
||||
this.chkExpand.setChecked(expanded);
|
||||
this.chkExpand.element.setAttribute("aria-expanded", expanded);
|
||||
}
|
||||
|
||||
// Specify the display mode of the register value
|
||||
setFormat(format) {
|
||||
this.format = format;
|
||||
this.setValue(this.value);
|
||||
this.element.setAttribute("format", format.replace("_", ""));
|
||||
}
|
||||
|
||||
// Update the value of the register
|
||||
setValue(value, pcFrom, pcTo) {
|
||||
this.value = value;
|
||||
|
||||
// Value text box
|
||||
let text;
|
||||
switch (this.format) {
|
||||
case "float":
|
||||
let e = value >> 23 & 0xFF;
|
||||
let s = value & 0x007FFFFF;
|
||||
|
||||
// Check for denormal number
|
||||
if (e == 0x00 & s != 0) {
|
||||
text = "Denormal";
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for reserved operand
|
||||
if (e == 0xFF) {
|
||||
text = s!=0 ? "NaN" : value<0 ? "-Infinity" : "Infinity";
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for negative zero
|
||||
if ((value & 0xFFFFFFFF) >>> 0 == 0x80000000) {
|
||||
text = "-0.0";
|
||||
break;
|
||||
}
|
||||
|
||||
// Format the number
|
||||
text = CPUWindow.Register.intBitsToFloat(value).toFixed(6);
|
||||
if (text.indexOf(".") == -1)
|
||||
text += ".0";
|
||||
while (text.endsWith("0"))
|
||||
text = text.substring(0, text.length - 1);
|
||||
if (text.endsWith("."))
|
||||
text += "0";
|
||||
break;
|
||||
case "hex":
|
||||
text = ("0000000" +
|
||||
(value >>> 0).toString(16).toUpperCase()).slice(-8);
|
||||
break;
|
||||
case "signed":
|
||||
text = CPUWindow.Register.asSigned(value).toString();
|
||||
break;
|
||||
case "unsigned":
|
||||
text = (value >>> 0).toString();
|
||||
}
|
||||
this.txtValue.setText(text);
|
||||
|
||||
// Expansion fields
|
||||
for (let field of Object.values(this.fields)) {
|
||||
switch (field.type) {
|
||||
case "bit":
|
||||
field.setChecked(value >> field.bit & 1);
|
||||
break;
|
||||
case "decimal":
|
||||
field.setText(value >> field.bit & field.mask);
|
||||
break;
|
||||
case "hex":
|
||||
let digits = Math.max(1, Math.ceil(field.width / 4));
|
||||
field.setText(("0".repeat(digits) +
|
||||
(value >> field.bit & field.mask)
|
||||
.toString(16).toUpperCase()
|
||||
).slice(-digits));
|
||||
}
|
||||
}
|
||||
|
||||
// Special fields for PC
|
||||
if (pcFrom === undefined)
|
||||
return;
|
||||
this.txtFrom.setText(("0000000" +
|
||||
(pcFrom >>> 0).toString(16).toUpperCase()).slice(-8));
|
||||
this.txtTo .setText(("0000000" +
|
||||
(pcTo >>> 0).toString(16).toUpperCase()).slice(-8));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Add a field component to the expansion area
|
||||
addField(type, name, bit, width, readonly) {
|
||||
let field, label, panel;
|
||||
|
||||
// Processing by type
|
||||
switch (type) {
|
||||
|
||||
// Bit
|
||||
case "bit":
|
||||
field = this.newCheckBox({
|
||||
enabled: !readonly,
|
||||
text : name
|
||||
});
|
||||
field.addChangeListener(e=>this.onbit(field));
|
||||
this.expansion.add(field);
|
||||
break;
|
||||
|
||||
// Decimal number
|
||||
case "decimal":
|
||||
|
||||
// Field
|
||||
field = this.newTextBox({
|
||||
enabled: !readonly,
|
||||
name : name
|
||||
});
|
||||
field.addCommitListener(e=>this.onnumber(field));
|
||||
label = this.newLabel({
|
||||
label: true,
|
||||
text : name
|
||||
});
|
||||
label.element.htmlFor = field.id;
|
||||
if (readonly)
|
||||
label.element.setAttribute("aria-disabled", "true");
|
||||
|
||||
// Enclose in a panel
|
||||
panel = this.newPanel({
|
||||
layout : "flex",
|
||||
alignCross: "center",
|
||||
alignMain : "start",
|
||||
direction : "row"
|
||||
});
|
||||
panel.element.setAttribute("name", name);
|
||||
panel.add(label);
|
||||
panel.add(field);
|
||||
this.expansion.add(panel);
|
||||
break;
|
||||
|
||||
// Hexadecimal number
|
||||
case "hex":
|
||||
field = this.newTextBox({
|
||||
enabled: !readonly,
|
||||
name : name
|
||||
});
|
||||
field.addCommitListener(e=>this.onnumber(field));
|
||||
label = this.newLabel({
|
||||
label: true,
|
||||
text : name
|
||||
});
|
||||
label.element.htmlFor = field.id;
|
||||
if (readonly)
|
||||
label.element.setAttribute("aria-disabled", "true");
|
||||
this.expansion.add(label);
|
||||
this.expansion.add(field);
|
||||
}
|
||||
|
||||
// Configure field
|
||||
field.bit = bit;
|
||||
field.mask = (1 << width) - 1;
|
||||
field.type = type;
|
||||
field.width = width;
|
||||
this.fields[name] = field;
|
||||
}
|
||||
|
||||
// Expansion controls for CHCW
|
||||
expansionCHCW() {
|
||||
|
||||
// Expansion area
|
||||
this.expansion = this.newPanel({
|
||||
layout : "block",
|
||||
overflowX: "visible",
|
||||
overflowY: "visible",
|
||||
visible : false
|
||||
});
|
||||
this.expansion.element.setAttribute("name", "expansion");
|
||||
this.expansion.element.setAttribute("register", "chcw");
|
||||
|
||||
// Fields
|
||||
this.addField("bit", "ICE", 1, 1, false);
|
||||
}
|
||||
|
||||
// Expansion controls for ECR
|
||||
expansionECR() {
|
||||
|
||||
// Expansion area
|
||||
this.expansion = this.newPanel({
|
||||
layout : "grid",
|
||||
columns : "max-content auto",
|
||||
overflowX: "visible",
|
||||
overflowY: "visible",
|
||||
visible : false
|
||||
});
|
||||
this.expansion.element.setAttribute("name", "expansion");
|
||||
this.expansion.element.setAttribute("register", "ecr");
|
||||
this.expansion.element.style.justifyContent = "start";
|
||||
|
||||
// Fields
|
||||
this.addField("hex", "FECC", 16, 16, false);
|
||||
this.addField("hex", "EICC", 0, 16, false);
|
||||
}
|
||||
|
||||
// Expansion controls for PC
|
||||
expansionPC() {
|
||||
|
||||
// Expansion area
|
||||
this.expansion = this.newPanel({
|
||||
layout : "grid",
|
||||
columns : "auto max-content",
|
||||
overflowX: "visible",
|
||||
overflowY: "visible",
|
||||
visible : false
|
||||
});
|
||||
this.expansion.element.setAttribute("name", "expansion");
|
||||
|
||||
// From text box
|
||||
let lbl = this.expansion.add(this.newLabel({
|
||||
localized: true,
|
||||
text : "{cpu.pcFrom}"
|
||||
}));
|
||||
this.txtFrom = this.expansion.add(this.newTextBox({ enabled: false }));
|
||||
this.txtFrom.element.setAttribute("name", "value");
|
||||
|
||||
// To text box
|
||||
lbl = this.expansion.add(this.newLabel({
|
||||
localized: true,
|
||||
text : "{cpu.pcTo}"
|
||||
}));
|
||||
this.txtTo = this.expansion.add(this.newTextBox({ enabled: false }));
|
||||
this.txtTo.element.setAttribute("name", "value");
|
||||
}
|
||||
|
||||
// Expansion controls for PIR
|
||||
expansionPIR() {
|
||||
|
||||
// Expansion area
|
||||
this.expansion = this.newPanel({
|
||||
layout : "grid",
|
||||
columns : "max-content auto",
|
||||
overflowX: "visible",
|
||||
overflowY: "visible",
|
||||
visible : false
|
||||
});
|
||||
this.expansion.element.setAttribute("name", "expansion");
|
||||
this.expansion.element.setAttribute("register", "pir");
|
||||
|
||||
// Fields
|
||||
this.addField("hex", "PT", 0, 16, true);
|
||||
}
|
||||
|
||||
// Expansion controls for program registers
|
||||
expansionProgram() {
|
||||
|
||||
// Expansion area
|
||||
this.expansion = this.newPanel({
|
||||
layout : "grid",
|
||||
columns : "auto",
|
||||
overflowX: "visible",
|
||||
overflowY: "visible",
|
||||
visible : false
|
||||
});
|
||||
this.expansion.element.setAttribute("role", "radiogroup");
|
||||
this.expansion.element.setAttribute("name", "expansion");
|
||||
this.expansion.element.setAttribute("register", "program");
|
||||
this.expansion.element.style.justifyContent = "start";
|
||||
|
||||
// Format selections
|
||||
let group = new Toolkit.ButtonGroup();
|
||||
for (let opt of [ "hex", "signed", "unsigned", "float_" ]) {
|
||||
let fmt = group.add(this.expansion.add(this.newRadioButton({
|
||||
checked: opt == "hex",
|
||||
text : "{cpu." + opt + "}",
|
||||
})));
|
||||
fmt.addChangeListener(
|
||||
(opt=>e=>this.setFormat(opt))
|
||||
(opt.replace("_", ""))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Expansion controls for EIPSW, FEPSW and PSW
|
||||
expansionPSW() {
|
||||
|
||||
// Expansion area
|
||||
this.expansion = this.newPanel({
|
||||
layout : "grid",
|
||||
columns : "max-content auto",
|
||||
overflowX: "visible",
|
||||
overflowY: "visible",
|
||||
visible : false
|
||||
});
|
||||
this.expansion.element.setAttribute("name", "expansion");
|
||||
this.expansion.element.setAttribute("register", "psw");
|
||||
this.expansion.element.style.justifyContent = "start";
|
||||
|
||||
// Fields
|
||||
this.addField("bit" , "CY" , 3, 1, false);
|
||||
this.addField("bit" , "FRO", 9, 1, false);
|
||||
this.addField("bit" , "OV" , 2, 1, false);
|
||||
this.addField("bit" , "FIV", 8, 1, false);
|
||||
this.addField("bit" , "S" , 1, 1, false);
|
||||
this.addField("bit" , "FZD", 7, 1, false);
|
||||
this.addField("bit" , "Z" , 0, 1, false);
|
||||
this.addField("bit" , "FOV", 6, 1, false);
|
||||
this.addField("bit" , "NP" , 15, 1, false);
|
||||
this.addField("bit" , "FUD", 5, 1, false);
|
||||
this.addField("bit" , "EP" , 14, 1, false);
|
||||
this.addField("bit" , "FPR", 4, 1, false);
|
||||
this.addField("bit" , "ID" , 12, 1, false);
|
||||
this.addField("decimal", "I" , 16, 4, false);
|
||||
this.addField("bit" , "AE" , 13, 1, false);
|
||||
}
|
||||
|
||||
// Expansion controls for TKCW
|
||||
expansionTKCW() {
|
||||
|
||||
// Expansion area
|
||||
this.expansion = this.newPanel({
|
||||
layout : "grid",
|
||||
columns : "max-content auto",
|
||||
overflowX: "visible",
|
||||
overflowY: "visible",
|
||||
visible : false
|
||||
});
|
||||
this.expansion.element.setAttribute("name", "expansion");
|
||||
this.expansion.element.setAttribute("register", "tkcw");
|
||||
this.expansion.element.style.justifyContent = "start";
|
||||
|
||||
// Fields
|
||||
this.addField("bit" , "FIT", 7, 1, true);
|
||||
this.addField("bit" , "FUT", 4, 1, true);
|
||||
this.addField("bit" , "FZT", 6, 1, true);
|
||||
this.addField("bit" , "FPT", 3, 1, true);
|
||||
this.addField("bit" , "FVT", 5, 1, true);
|
||||
this.addField("bit" , "OTM", 8, 1, true);
|
||||
this.addField("bit" , "RDI", 2, 1, true);
|
||||
this.addField("decimal", "RD" , 0, 2, true);
|
||||
}
|
||||
|
||||
// Bit check box change event handler
|
||||
onbit(field) {
|
||||
let mask = 1 << field.bit;
|
||||
let value = this.value;
|
||||
if (field.isChecked())
|
||||
value = (value | mask & 0xFFFFFFFF) >>> 0;
|
||||
else value = (value & ~mask & 0xFFFFFFFF) >>> 0;
|
||||
this.setRegister(value);
|
||||
}
|
||||
|
||||
// Number text box commit event handler
|
||||
onnumber(field) {
|
||||
let value = parseInt(field.getText(),
|
||||
field.type == "decimal" ? 10 : 16);
|
||||
if (value == NaN)
|
||||
value = this.value;
|
||||
this.setRegister((
|
||||
this.value & ~(field.mask << field.bit) |
|
||||
(value & field.mask) << field.bit
|
||||
) >>> 0);
|
||||
}
|
||||
|
||||
// Value text box commit event handler
|
||||
onvalue() {
|
||||
|
||||
// Process the entered value
|
||||
let value = this.txtValue.getText();
|
||||
switch (this.format) {
|
||||
case "float":
|
||||
value = parseFloat(value);
|
||||
if (Number.isFinite(value))
|
||||
value = CPUWindow.Register.floatToIntBits(value);
|
||||
break;
|
||||
case "hex":
|
||||
value = parseInt(value, 16);
|
||||
break;
|
||||
case "signed":
|
||||
case "unsigned":
|
||||
value = parseInt(value);
|
||||
}
|
||||
|
||||
// Update the value
|
||||
if (!Number.isFinite(value))
|
||||
this.setValue(this.value);
|
||||
else this.setRegister((value & 0xFFFFFFFF) >>> 0);
|
||||
}
|
||||
|
||||
// Update the value of the register
|
||||
setRegister(value) {
|
||||
this.debug.core.postMessage({
|
||||
command: "SetRegister",
|
||||
dbgwnd : "CPU",
|
||||
id : this.id,
|
||||
sim : this.debug.sim,
|
||||
type : this.system ? this.id == -1 ? "pc" : "system" : "program",
|
||||
value : value
|
||||
});
|
||||
}
|
||||
|
||||
}).initializer();
|
32
core/bus.c
32
core/bus.c
|
@ -64,7 +64,7 @@ static void busWriteMemory(uint8_t *mem, int type, int32_t value) {
|
|||
/***************************** Module Functions ******************************/
|
||||
|
||||
/* Read a data unit from the bus */
|
||||
static int32_t busRead(VB *emu, uint32_t address, int type, int debug) {
|
||||
static int32_t busRead(VB *sim, uint32_t address, int type, int debug) {
|
||||
|
||||
/* Force address alignment */
|
||||
address &= ~((uint32_t) TYPE_SIZES[type] - 1);
|
||||
|
@ -77,20 +77,20 @@ static int32_t busRead(VB *emu, uint32_t address, int type, int debug) {
|
|||
case 3 : return 0; /* Unmapped */
|
||||
case 4 : return 0; /* Game pak expansion */
|
||||
case 5 : return /* WRAM */
|
||||
busReadMemory(&emu->wram[address & 0xFFFF], type);
|
||||
case 6 : return emu->cart.sram == NULL ? 0 : /* Game pak RAM */
|
||||
busReadMemory(&emu->cart.sram
|
||||
[address & (emu->cart.sramSize - 1)], type);
|
||||
default: return emu->cart.rom == NULL ? 0 : /* Game pak ROM */
|
||||
busReadMemory(&emu->cart.rom
|
||||
[address & (emu->cart.romSize - 1)], type);
|
||||
busReadMemory(&sim->wram[address & 0xFFFF], type);
|
||||
case 6 : return sim->cart.sram == NULL ? 0 : /* Game pak RAM */
|
||||
busReadMemory(&sim->cart.sram
|
||||
[address & (sim->cart.sramSize - 1)], type);
|
||||
default: return sim->cart.rom == NULL ? 0 : /* Game pak ROM */
|
||||
busReadMemory(&sim->cart.rom
|
||||
[address & (sim->cart.romSize - 1)], type);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Write a data unit to the bus */
|
||||
static void busWrite(
|
||||
VB *emu, uint32_t address, int type, uint32_t value, int debug) {
|
||||
VB *sim, uint32_t address, int type, uint32_t value, int debug) {
|
||||
|
||||
/* Force address alignment */
|
||||
address &= ~((uint32_t) TYPE_SIZES[type] - 1);
|
||||
|
@ -103,17 +103,17 @@ static void busWrite(
|
|||
case 3 : return; /* Unmapped */
|
||||
case 4 : return; /* Game pak expansion */
|
||||
case 5 : /* WRAM */
|
||||
busWriteMemory(&emu->wram[address & 0xFFFF], type, value);
|
||||
busWriteMemory(&sim->wram[address & 0xFFFF], type, value);
|
||||
return;
|
||||
case 6 : /* Cartridge RAM */
|
||||
if (emu->cart.sram != NULL)
|
||||
busWriteMemory(&emu->cart.sram
|
||||
[address & (emu->cart.sramSize - 1)], type, value);
|
||||
if (sim->cart.sram != NULL)
|
||||
busWriteMemory(&sim->cart.sram
|
||||
[address & (sim->cart.sramSize - 1)], type, value);
|
||||
return;
|
||||
default: /* Cartridge ROM */
|
||||
if (debug && emu->cart.rom != NULL)
|
||||
busWriteMemory(&emu->cart.rom
|
||||
[address & (emu->cart.romSize - 1)], type, value);
|
||||
if (debug && sim->cart.rom != NULL)
|
||||
busWriteMemory(&sim->cart.rom
|
||||
[address & (sim->cart.romSize - 1)], type, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
1372
core/cpu.c
1372
core/cpu.c
File diff suppressed because it is too large
Load Diff
265
core/vb.c
265
core/vb.c
|
@ -1,4 +1,4 @@
|
|||
#define VBAPI
|
||||
#define VBAPI VB_EXPORT
|
||||
|
||||
/* Header includes */
|
||||
#include <float.h>
|
||||
|
@ -33,15 +33,15 @@ static const uint8_t TYPE_SIZES[] = { 1, 1, 2, 2, 4 };
|
|||
/***************************** Module Functions ******************************/
|
||||
|
||||
/* Process a simulation for some number of clocks */
|
||||
static int sysEmulate(VB *emu, uint32_t clocks) {
|
||||
static int sysEmulate(VB *sim, uint32_t clocks) {
|
||||
int broke;
|
||||
broke = cpuEmulate(emu, clocks);
|
||||
broke = cpuEmulate(sim, clocks);
|
||||
return broke;
|
||||
}
|
||||
|
||||
/* Determine the number of clocks before a break condititon could occur */
|
||||
static uint32_t sysUntil(VB *emu, uint32_t clocks) {
|
||||
clocks = cpuUntil(emu, clocks);
|
||||
static uint32_t sysUntil(VB *sim, uint32_t clocks) {
|
||||
clocks = cpuUntil(sim, clocks);
|
||||
return clocks;
|
||||
}
|
||||
|
||||
|
@ -49,242 +49,253 @@ static uint32_t sysUntil(VB *emu, uint32_t clocks) {
|
|||
|
||||
/******************************* API Functions *******************************/
|
||||
|
||||
/* Associate two simulations as peers */
|
||||
void vbConnect(VB *emu1, VB *emu2) {
|
||||
/* Associate two simulations as peers, or remove an association */
|
||||
void vbConnect(VB *a, VB *b) {
|
||||
|
||||
/* Disconnect */
|
||||
if (b == NULL) {
|
||||
if (a->peer != NULL)
|
||||
a->peer->peer = NULL;
|
||||
a->peer = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
/* The simulations are already linked */
|
||||
if (a->peer == b && b->peer == a)
|
||||
return;
|
||||
|
||||
/* Disconnect any existing link associations */
|
||||
if (emu1->peer != NULL && emu1->peer != emu2)
|
||||
emu1->peer->peer = NULL;
|
||||
if (emu2->peer != NULL && emu2->peer != emu1)
|
||||
emu2->peer->peer = NULL;
|
||||
if (a->peer != NULL && a->peer != b)
|
||||
a->peer->peer = NULL;
|
||||
if (b->peer != NULL && b->peer != a)
|
||||
b->peer->peer = NULL;
|
||||
|
||||
/* Link the two simulations */
|
||||
emu1->peer = emu2;
|
||||
emu2->peer = emu1;
|
||||
a->peer = b;
|
||||
b->peer = a;
|
||||
}
|
||||
|
||||
/* Disassociate linked peers */
|
||||
void vbDisconnect(VB *emu) {
|
||||
if (emu->peer != NULL)
|
||||
emu->peer->peer = NULL;
|
||||
emu->peer = NULL;
|
||||
}
|
||||
|
||||
/* Process one or two simulations */
|
||||
int vbEmulate(VB *emu1, VB *emu2, uint32_t *clocks) {
|
||||
/* Process one simulation */
|
||||
int vbEmulate(VB *sim, uint32_t *clocks) {
|
||||
int broke; /* The simulation requested an application break */
|
||||
uint32_t until; /* Maximum clocks before a break could happen */
|
||||
int x; /* Iterator */
|
||||
|
||||
/* Processing one simulaiton */
|
||||
if (emu2 == NULL) {
|
||||
do {
|
||||
until = sysUntil (emu1, *clocks);
|
||||
broke = sysEmulate(emu1, until );
|
||||
*clocks -= until;
|
||||
} while (!broke && *clocks > 0);
|
||||
}
|
||||
/* Process the simulation until a break condition occurs */
|
||||
do {
|
||||
until = *clocks;
|
||||
until = sysUntil (sim, until);
|
||||
broke = sysEmulate(sim, until);
|
||||
*clocks -= until;
|
||||
} while (!broke && *clocks > 0);
|
||||
|
||||
/* Processing two simulations */
|
||||
else {
|
||||
do {
|
||||
until = sysUntil (emu1, *clocks);
|
||||
until = sysUntil (emu2, until );
|
||||
broke = sysEmulate(emu1, until );
|
||||
broke |= sysEmulate(emu2, until );
|
||||
*clocks -= until;
|
||||
} while (!broke && *clocks > 0);
|
||||
}
|
||||
return broke;
|
||||
}
|
||||
|
||||
/* Process multiple simulations */
|
||||
int vbEmulateMulti(VB **sims, int count, uint32_t *clocks) {
|
||||
int broke; /* The simulation requested an application break */
|
||||
uint32_t until; /* Maximum clocks before a break could happen */
|
||||
int x; /* Iterator */
|
||||
|
||||
/* Process simulations until a break condition occurs */
|
||||
do {
|
||||
broke = 0;
|
||||
until = *clocks;
|
||||
for (x = 0; x < count; x++)
|
||||
until = sysUntil (sims[x], until);
|
||||
for (x = 0; x < count; x++)
|
||||
broke |= sysEmulate(sims[x], until);
|
||||
*clocks -= until;
|
||||
} while (!broke && *clocks > 0);
|
||||
|
||||
return broke;
|
||||
}
|
||||
|
||||
/* Retrieve a current breakpoint callback */
|
||||
void* vbGetCallback(VB *emu, int type) {
|
||||
void* vbGetCallback(VB *sim, int type) {
|
||||
/* -Wpedantic ignored for pointer conversion because no alternative */
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpedantic"
|
||||
switch (type) {
|
||||
case VB_ONEXCEPTION: return emu->onException;
|
||||
case VB_ONEXECUTE : return emu->onExecute;
|
||||
case VB_ONFETCH : return emu->onFetch;
|
||||
case VB_ONREAD : return emu->onRead;
|
||||
case VB_ONWRITE : return emu->onWrite;
|
||||
case VB_ONEXCEPTION: return sim->onException;
|
||||
case VB_ONEXECUTE : return sim->onExecute;
|
||||
case VB_ONFETCH : return sim->onFetch;
|
||||
case VB_ONREAD : return sim->onRead;
|
||||
case VB_ONWRITE : return sim->onWrite;
|
||||
}
|
||||
#pragma GCC diagnostic pop
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Retrieve the value of PC */
|
||||
uint32_t vbGetProgramCounter(VB *emu, int type) {
|
||||
switch (type) {
|
||||
case VB_PC : return emu->cpu.pc;
|
||||
case VB_PC_FROM: return emu->cpu.pcFrom;
|
||||
case VB_PC_TO : return emu->cpu.pcTo;
|
||||
}
|
||||
return 0;
|
||||
uint32_t vbGetProgramCounter(VB *sim) {
|
||||
return sim->cpu.pc;
|
||||
}
|
||||
|
||||
/* Retrieve the value of a program register */
|
||||
int32_t vbGetProgramRegister(VB *emu, int id) {
|
||||
return id < 1 || id > 31 ? 0 : emu->cpu.program[id];
|
||||
int32_t vbGetProgramRegister(VB *sim, int id) {
|
||||
return id < 1 || id > 31 ? 0 : sim->cpu.program[id];
|
||||
}
|
||||
|
||||
/* Retrieve the ROM buffer */
|
||||
void* vbGetROM(VB *emu, uint32_t *size) {
|
||||
void* vbGetROM(VB *sim, uint32_t *size) {
|
||||
if (size != NULL)
|
||||
*size = emu->cart.romSize;
|
||||
return emu->cart.rom;
|
||||
*size = sim->cart.romSize;
|
||||
return sim->cart.rom;
|
||||
}
|
||||
|
||||
/* Retrieve the SRAM buffer */
|
||||
void* vbGetSRAM(VB *emu, uint32_t *size) {
|
||||
void* vbGetSRAM(VB *sim, uint32_t *size) {
|
||||
if (size != NULL)
|
||||
*size = emu->cart.sramSize;
|
||||
return emu->cart.sram;
|
||||
*size = sim->cart.sramSize;
|
||||
return sim->cart.sram;
|
||||
}
|
||||
|
||||
/* Retrieve the value of a system register */
|
||||
uint32_t vbGetSystemRegister(VB *emu, int id) {
|
||||
uint32_t vbGetSystemRegister(VB *sim, int id) {
|
||||
switch (id) {
|
||||
case VB_ADTRE: return emu->cpu.adtre;
|
||||
case VB_CHCW : return emu->cpu.chcw.ice << 1;
|
||||
case VB_EIPC : return emu->cpu.eipc;
|
||||
case VB_EIPSW: return emu->cpu.eipsw;
|
||||
case VB_FEPC : return emu->cpu.fepc;
|
||||
case VB_FEPSW: return emu->cpu.fepsw;
|
||||
case VB_ADTRE: return sim->cpu.adtre;
|
||||
case VB_CHCW : return sim->cpu.chcw.ice << 1;
|
||||
case VB_EIPC : return sim->cpu.eipc;
|
||||
case VB_EIPSW: return sim->cpu.eipsw;
|
||||
case VB_FEPC : return sim->cpu.fepc;
|
||||
case VB_FEPSW: return sim->cpu.fepsw;
|
||||
case VB_PIR : return 0x00005346;
|
||||
case VB_TKCW : return 0x000000E0;
|
||||
case 29 : return emu->cpu.sr29;
|
||||
case 29 : return sim->cpu.sr29;
|
||||
case 30 : return 0x00000004;
|
||||
case 31 : return emu->cpu.sr31;
|
||||
case 31 : return sim->cpu.sr31;
|
||||
case VB_ECR : return
|
||||
(uint32_t) emu->cpu.ecr.fecc << 16 | emu->cpu.ecr.eicc;
|
||||
(uint32_t) sim->cpu.ecr.fecc << 16 | sim->cpu.ecr.eicc;
|
||||
case VB_PSW : return
|
||||
(uint32_t) emu->cpu.psw.i << 16 |
|
||||
(uint32_t) emu->cpu.psw.np << 15 |
|
||||
(uint32_t) emu->cpu.psw.ep << 14 |
|
||||
(uint32_t) emu->cpu.psw.ae << 13 |
|
||||
(uint32_t) emu->cpu.psw.id << 12 |
|
||||
(uint32_t) emu->cpu.psw.fro << 9 |
|
||||
(uint32_t) emu->cpu.psw.fiv << 8 |
|
||||
(uint32_t) emu->cpu.psw.fzd << 7 |
|
||||
(uint32_t) emu->cpu.psw.fov << 6 |
|
||||
(uint32_t) emu->cpu.psw.fud << 5 |
|
||||
(uint32_t) emu->cpu.psw.fpr << 4 |
|
||||
(uint32_t) emu->cpu.psw.cy << 3 |
|
||||
(uint32_t) emu->cpu.psw.ov << 2 |
|
||||
(uint32_t) emu->cpu.psw.s << 1 |
|
||||
(uint32_t) emu->cpu.psw.z
|
||||
(uint32_t) sim->cpu.psw.i << 16 |
|
||||
(uint32_t) sim->cpu.psw.np << 15 |
|
||||
(uint32_t) sim->cpu.psw.ep << 14 |
|
||||
(uint32_t) sim->cpu.psw.ae << 13 |
|
||||
(uint32_t) sim->cpu.psw.id << 12 |
|
||||
(uint32_t) sim->cpu.psw.fro << 9 |
|
||||
(uint32_t) sim->cpu.psw.fiv << 8 |
|
||||
(uint32_t) sim->cpu.psw.fzd << 7 |
|
||||
(uint32_t) sim->cpu.psw.fov << 6 |
|
||||
(uint32_t) sim->cpu.psw.fud << 5 |
|
||||
(uint32_t) sim->cpu.psw.fpr << 4 |
|
||||
(uint32_t) sim->cpu.psw.cy << 3 |
|
||||
(uint32_t) sim->cpu.psw.ov << 2 |
|
||||
(uint32_t) sim->cpu.psw.s << 1 |
|
||||
(uint32_t) sim->cpu.psw.z
|
||||
;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Prepare a simulation state instance for use */
|
||||
void vbInit(VB *emu) {
|
||||
void vbInit(VB *sim) {
|
||||
|
||||
/* Breakpoint callbacks */
|
||||
emu->onException = NULL;
|
||||
emu->onExecute = NULL;
|
||||
emu->onFetch = NULL;
|
||||
emu->onRead = NULL;
|
||||
emu->onWrite = NULL;
|
||||
sim->onException = NULL;
|
||||
sim->onExecute = NULL;
|
||||
sim->onFetch = NULL;
|
||||
sim->onRead = NULL;
|
||||
sim->onWrite = NULL;
|
||||
|
||||
/* System */
|
||||
emu->peer = NULL;
|
||||
sim->peer = NULL;
|
||||
|
||||
/* Cartridge */
|
||||
emu->cart.rom = NULL;
|
||||
emu->cart.romSize = 0;
|
||||
emu->cart.sram = NULL;
|
||||
emu->cart.sramSize = 0;
|
||||
sim->cart.rom = NULL;
|
||||
sim->cart.romSize = 0;
|
||||
sim->cart.sram = NULL;
|
||||
sim->cart.sramSize = 0;
|
||||
|
||||
/* Everything else */
|
||||
vbReset(emu);
|
||||
vbReset(sim);
|
||||
}
|
||||
|
||||
/* Read a data unit from the bus */
|
||||
int32_t vbRead(VB *emu, uint32_t address, int type, int debug) {
|
||||
int32_t vbRead(VB *sim, uint32_t address, int type, int debug) {
|
||||
return type < 0 || type >= (int) sizeof TYPE_SIZES ? 0 :
|
||||
busRead(emu, address, type, debug);
|
||||
busRead(sim, address, type, debug);
|
||||
}
|
||||
|
||||
/* Simulate a hardware reset */
|
||||
void vbReset(VB *emu) {
|
||||
void vbReset(VB *sim) {
|
||||
uint32_t x; /* Iterator */
|
||||
|
||||
/* Subsystem components */
|
||||
cpuReset(emu);
|
||||
cpuReset(sim);
|
||||
|
||||
/* WRAM (the hardware does not do this) */
|
||||
for (x = 0; x < 0x10000; x++)
|
||||
emu->wram[x] = 0x00;
|
||||
sim->wram[x] = 0x00;
|
||||
}
|
||||
|
||||
/* Specify a breakpoint callback */
|
||||
void vbSetCallback(VB *emu, int type, void *callback) {
|
||||
void vbSetCallback(VB *sim, int type, void *callback) {
|
||||
/* -Wpedantic ignored for pointer conversion because no alternative */
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpedantic"
|
||||
switch (type) {
|
||||
case VB_ONEXCEPTION: emu->onException=(VB_EXCEPTIONPROC)callback;break;
|
||||
case VB_ONEXECUTE : emu->onExecute =(VB_EXECUTEPROC )callback;break;
|
||||
case VB_ONFETCH : emu->onFetch =(VB_FETCHPROC )callback;break;
|
||||
case VB_ONREAD : emu->onRead =(VB_READPROC )callback;break;
|
||||
case VB_ONWRITE : emu->onWrite =(VB_WRITEPROC )callback;break;
|
||||
case VB_ONEXCEPTION: sim->onException=(VB_EXCEPTIONPROC)callback;break;
|
||||
case VB_ONEXECUTE : sim->onExecute =(VB_EXECUTEPROC )callback;break;
|
||||
case VB_ONFETCH : sim->onFetch =(VB_FETCHPROC )callback;break;
|
||||
case VB_ONREAD : sim->onRead =(VB_READPROC )callback;break;
|
||||
case VB_ONWRITE : sim->onWrite =(VB_WRITEPROC )callback;break;
|
||||
}
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
|
||||
/* Specify a new value for PC */
|
||||
uint32_t vbSetProgramCounter(VB *emu, uint32_t value) {
|
||||
uint32_t vbSetProgramCounter(VB *sim, uint32_t value) {
|
||||
value &= 0xFFFFFFFE;
|
||||
emu->cpu.causeCode = 0;
|
||||
emu->cpu.fetch = 0;
|
||||
emu->cpu.pc = value;
|
||||
emu->cpu.state = CPU_FETCH;
|
||||
emu->cpu.substring = 0;
|
||||
sim->cpu.busWait = 0;
|
||||
sim->cpu.causeCode = 0;
|
||||
sim->cpu.clocks = 0;
|
||||
sim->cpu.fetch = 0;
|
||||
sim->cpu.pc = value;
|
||||
sim->cpu.state = CPU_FETCH;
|
||||
sim->cpu.substring = 0;
|
||||
return value;
|
||||
}
|
||||
|
||||
/* Specify a new value for a program register */
|
||||
int32_t vbSetProgramRegister(VB *emu, int id, int32_t value) {
|
||||
return id < 1 || id > 31 ? 0 : (emu->cpu.program[id] = value);
|
||||
int32_t vbSetProgramRegister(VB *sim, int id, int32_t value) {
|
||||
return id < 1 || id > 31 ? 0 : (sim->cpu.program[id] = value);
|
||||
}
|
||||
|
||||
/* Supply a ROM buffer */
|
||||
int vbSetROM(VB *emu, void *rom, uint32_t size) {
|
||||
int vbSetROM(VB *sim, void *rom, uint32_t size) {
|
||||
|
||||
/* Check the buffer size */
|
||||
if (size < 1024 || size > 0x1000000 || ((size - 1) & size) != 0)
|
||||
return 0;
|
||||
|
||||
/* Configure the ROM buffer */
|
||||
emu->cart.rom = (uint8_t *) rom;
|
||||
emu->cart.romSize = size;
|
||||
sim->cart.rom = (uint8_t *) rom;
|
||||
sim->cart.romSize = size;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Supply an SRAM buffer */
|
||||
int vbSetSRAM(VB *emu, void *sram, uint32_t size) {
|
||||
int vbSetSRAM(VB *sim, void *sram, uint32_t size) {
|
||||
|
||||
/* Check the buffer size */
|
||||
if (size == 0 || ((size - 1) & size) != 0)
|
||||
return 0;
|
||||
|
||||
/* Configure the SRAM buffer */
|
||||
emu->cart.sram = (uint8_t *) sram;
|
||||
emu->cart.sramSize = size;
|
||||
sim->cart.sram = (uint8_t *) sram;
|
||||
sim->cart.sramSize = size;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Specify a new value for a system register */
|
||||
uint32_t vbSetSystemRegister(VB *emu, int id, uint32_t value) {
|
||||
return cpuSetSystemRegister(emu, id, value, 1);
|
||||
uint32_t vbSetSystemRegister(VB *sim, int id, uint32_t value) {
|
||||
return cpuSetSystemRegister(sim, id, value, 1);
|
||||
}
|
||||
|
||||
/* Write a data unit to the bus */
|
||||
void vbWrite(VB *emu, uint32_t address, int type, int32_t value, int debug) {
|
||||
void vbWrite(VB *sim, uint32_t address, int type, int32_t value, int debug) {
|
||||
if (type >= 0 && type < (int32_t) sizeof TYPE_SIZES)
|
||||
busWrite(emu, address, type, value, debug);
|
||||
busWrite(sim, address, type, value, debug);
|
||||
}
|
||||
|
|
48
core/vb.h
48
core/vb.h
|
@ -141,14 +141,6 @@ struct VB {
|
|||
uint32_t pc; /* Program counter */
|
||||
int32_t program[32]; /* program registers */
|
||||
|
||||
/* History tracking */
|
||||
uint32_t eipcFrom; /* Source of most recent jump */
|
||||
uint32_t eipcTo; /* Destination of most recent jump */
|
||||
uint32_t fepcFrom; /* Source of most recent jump */
|
||||
uint32_t fepcTo; /* Destination of most recent jump */
|
||||
uint32_t pcFrom; /* Source of most recent jump */
|
||||
uint32_t pcTo; /* Destination of most recent jump */
|
||||
|
||||
/* Other fields */
|
||||
VB_ACCESS access; /* Memory access descriptor */
|
||||
VB_INSTRUCTION inst; /* Instruction descriptor */
|
||||
|
@ -175,27 +167,27 @@ struct VB {
|
|||
|
||||
|
||||
|
||||
/**************************** Function Prototypes ****************************/
|
||||
/******************************* API Commands ********************************/
|
||||
|
||||
VBAPI void vbConnect (VB *emu1, VB *emu2);
|
||||
VBAPI void vbDisconnect (VB *emu);
|
||||
VBAPI int vbEmulate (VB *emu1, VB *emu2, uint32_t *clocks);
|
||||
VBAPI void* vbGetCallback (VB *emu, int type);
|
||||
VBAPI uint32_t vbGetProgramCounter (VB *emu, int type);
|
||||
VBAPI int32_t vbGetProgramRegister (VB *emu, int id);
|
||||
VBAPI void* vbGetROM (VB *emu, uint32_t *size);
|
||||
VBAPI void* vbGetSRAM (VB *emu, uint32_t *size);
|
||||
VBAPI uint32_t vbGetSystemRegister (VB *emu, int id);
|
||||
VBAPI void vbInit (VB *emu);
|
||||
VBAPI int32_t vbRead (VB *emu, uint32_t address, int type, int debug);
|
||||
VBAPI void vbReset (VB *emu);
|
||||
VBAPI void vbSetCallback (VB *emu, int type, void *callback);
|
||||
VBAPI uint32_t vbSetProgramCounter (VB *emu, uint32_t value);
|
||||
VBAPI int32_t vbSetProgramRegister (VB *emu, int id, int32_t value);
|
||||
VBAPI int vbSetROM (VB *emu, void *rom, uint32_t size);
|
||||
VBAPI int vbSetSRAM (VB *emu, void *sram, uint32_t size);
|
||||
VBAPI uint32_t vbSetSystemRegister (VB *emu, int id, uint32_t value);
|
||||
VBAPI void vbWrite (VB *emu, uint32_t address, int type, int32_t value, int debug);
|
||||
VBAPI void vbConnect (VB *sim1, VB *sim2);
|
||||
VBAPI int vbEmulate (VB *sim, uint32_t *clocks);
|
||||
VBAPI int vbEmulateMulti (VB **sims, int count, uint32_t *clocks);
|
||||
VBAPI void* vbGetCallback (VB *sim, int type);
|
||||
VBAPI uint32_t vbGetProgramCounter (VB *sim);
|
||||
VBAPI int32_t vbGetProgramRegister (VB *sim, int id);
|
||||
VBAPI void* vbGetROM (VB *sim, uint32_t *size);
|
||||
VBAPI void* vbGetSRAM (VB *sim, uint32_t *size);
|
||||
VBAPI uint32_t vbGetSystemRegister (VB *sim, int id);
|
||||
VBAPI void vbInit (VB *sim);
|
||||
VBAPI int32_t vbRead (VB *sim, uint32_t address, int type, int debug);
|
||||
VBAPI void vbReset (VB *sim);
|
||||
VBAPI void vbSetCallback (VB *sim, int type, void *callback);
|
||||
VBAPI uint32_t vbSetProgramCounter (VB *sim, uint32_t value);
|
||||
VBAPI int32_t vbSetProgramRegister (VB *sim, int id, int32_t value);
|
||||
VBAPI int vbSetROM (VB *sim, void *rom, uint32_t size);
|
||||
VBAPI int vbSetSRAM (VB *sim, void *sram, uint32_t size);
|
||||
VBAPI uint32_t vbSetSystemRegister (VB *sim, int id, uint32_t value);
|
||||
VBAPI void vbWrite (VB *sim, uint32_t address, int type, int32_t value, int debug);
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Copyright (C) 2021 Guy Perfect
|
||||
Copyright (C) 2022 Guy Perfect
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
|
|
14
makefile
14
makefile
|
@ -1,7 +1,7 @@
|
|||
.PHONY: help
|
||||
help:
|
||||
@echo
|
||||
@echo "Virtual Boy Emulator - September 30, 2021"
|
||||
@echo "Virtual Boy Emulator - April 14, 2022"
|
||||
@echo
|
||||
@echo "Target build environment is any Debian with the following packages:"
|
||||
@echo " emscripten"
|
||||
|
@ -31,7 +31,7 @@ bundle:
|
|||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@rm -f vbemu_*.html core.wasm
|
||||
@rm -f vbemu_*.html app/core/core.wasm
|
||||
|
||||
.PHONY: core
|
||||
core:
|
||||
|
@ -42,7 +42,9 @@ core:
|
|||
|
||||
.PHONY: wasm
|
||||
wasm:
|
||||
@emcc -o core.wasm wasm/wasm.c core/vb.c -Icore -D VB_LITTLEENDIAN \
|
||||
--no-entry -O2 -flto -s WASM=1 -s EXPORTED_RUNTIME_METHODS=[] \
|
||||
-s ALLOW_MEMORY_GROWTH -s MAXIMUM_MEMORY=4GB -fno-strict-aliasing
|
||||
@rm -f *.wasm.tmp*
|
||||
@emcc -o app/core/core.wasm wasm/wasm.c core/vb.c -Icore \
|
||||
-D VB_LITTLEENDIAN --no-entry -O2 -flto -s WASM=1 \
|
||||
-D "VB_EXPORT=__attribute__((used))" \
|
||||
-s EXPORTED_RUNTIME_METHODS=[] -s ALLOW_MEMORY_GROWTH \
|
||||
-s MAXIMUM_MEMORY=4GB -fno-strict-aliasing
|
||||
@rm -f app/core/*.wasm.tmp*
|
||||
|
|
205
wasm/wasm.c
205
wasm/wasm.c
|
@ -1,21 +1,31 @@
|
|||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <vb.h>
|
||||
|
||||
|
||||
|
||||
//////////////////////////////// Static Memory ////////////////////////////////
|
||||
|
||||
static VB sims[2]; // Hardware simulations
|
||||
|
||||
|
||||
|
||||
/////////////////////////////// Module Commands ///////////////////////////////
|
||||
|
||||
// Allocate and initialize multiple simulations
|
||||
EMSCRIPTEN_KEEPALIVE VB** Create(int count) {
|
||||
VB **sims = malloc(count * sizeof (void *));
|
||||
for (int x = 0; x < count; x++)
|
||||
vbReset(sims[x] = malloc(sizeof (VB)));
|
||||
return sims;
|
||||
}
|
||||
|
||||
// Delete a simulation
|
||||
EMSCRIPTEN_KEEPALIVE void Destroy(VB *sim) {
|
||||
free(&sim->cart.rom);
|
||||
free(&sim->cart.sram);
|
||||
free(sim);
|
||||
}
|
||||
|
||||
// Proxy for free()
|
||||
EMSCRIPTEN_KEEPALIVE void Free(void* ptr) {
|
||||
EMSCRIPTEN_KEEPALIVE void Free(void *ptr) {
|
||||
free(ptr);
|
||||
}
|
||||
|
||||
|
@ -24,116 +34,97 @@ EMSCRIPTEN_KEEPALIVE void* Malloc(int size) {
|
|||
return malloc(size);
|
||||
}
|
||||
|
||||
// Read multiple data units from the bus
|
||||
// Determine the size in bytes of a pointer
|
||||
EMSCRIPTEN_KEEPALIVE int PointerSize() {
|
||||
return sizeof (void *);
|
||||
}
|
||||
|
||||
// Read multiple bytes from the bus
|
||||
EMSCRIPTEN_KEEPALIVE void ReadBuffer(
|
||||
int sim, uint8_t *dest, uint32_t address, uint32_t size, int debug) {
|
||||
VB* sim, uint8_t *dest, uint32_t address, uint32_t size) {
|
||||
for (; size > 0; address++, size--, dest++)
|
||||
*dest = vbRead(&sims[sim], address, VB_U8, debug);
|
||||
*dest = vbRead(sim, address, VB_U8, 1);
|
||||
}
|
||||
|
||||
// Supply a ROM buffer
|
||||
EMSCRIPTEN_KEEPALIVE int SetROM(VB *sim, uint8_t *rom, uint32_t size) {
|
||||
uint8_t *prev = vbGetROM(sim, NULL);
|
||||
int ret = vbSetROM(sim, rom, size);
|
||||
if (ret) {
|
||||
free(prev);
|
||||
vbReset(sim);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Write multiple bytes to the bus
|
||||
EMSCRIPTEN_KEEPALIVE void WriteBuffer(
|
||||
VB* sim, uint8_t *src, uint32_t address, uint32_t size) {
|
||||
for (; size > 0; address++, size--, src++)
|
||||
vbWrite(sim, address, VB_U8, *src, 1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////// Debugger Commands //////////////////////////////
|
||||
|
||||
// Attempt to execute until the following instruction
|
||||
static uint32_t RunNextPC;
|
||||
static int RunNextProcB(VB *sim, int fetch, VB_ACCESS *acc) {
|
||||
if (fetch == 0 && vbGetProgramCounter(sim) == RunNextPC)
|
||||
return 1;
|
||||
acc->value = vbRead(sim, acc->address, acc->type, 0);
|
||||
return 0;
|
||||
}
|
||||
static int RunNextProcA(VB *sim, VB_INSTRUCTION *inst) {
|
||||
RunNextPC = vbGetProgramCounter(sim) + inst->size;
|
||||
vbSetCallback(sim, VB_ONEXECUTE, NULL);
|
||||
vbSetCallback(sim, VB_ONFETCH, &RunNextProcB);
|
||||
return 0;
|
||||
}
|
||||
EMSCRIPTEN_KEEPALIVE void RunNext(VB *sim0, VB *sim1) {
|
||||
uint32_t clocks = 400000; // 1/50s
|
||||
VB *sims[2];
|
||||
|
||||
vbSetCallback(sim0, VB_ONEXECUTE, &RunNextProcA);
|
||||
|
||||
if (sim1 != NULL) {
|
||||
sims[0] = sim0;
|
||||
sims[1] = sim1;
|
||||
vbEmulateMulti(sims, 2, &clocks);
|
||||
}
|
||||
|
||||
else vbEmulate(sim0, &clocks);
|
||||
|
||||
vbSetCallback(sim0, VB_ONFETCH, NULL);
|
||||
}
|
||||
|
||||
// Execute one instruction
|
||||
static uint32_t SingleStepPC;
|
||||
static int SingleStepProc(VB *emu, int fetch, VB_ACCESS *acc) {
|
||||
if (fetch == 0 && vbGetProgramCounter(emu, VB_PC) != SingleStepPC)
|
||||
static int SingleStepProc(VB *sim, int fetch, VB_ACCESS *acc) {
|
||||
if (fetch == 0 && vbGetProgramCounter(sim) != SingleStepPC)
|
||||
return 1;
|
||||
acc->value = vbRead(emu, acc->address, acc->type, 0);
|
||||
acc->value = vbRead(sim, acc->address, acc->type, 0);
|
||||
return 0;
|
||||
}
|
||||
EMSCRIPTEN_KEEPALIVE void SingleStep(int sim) {
|
||||
EMSCRIPTEN_KEEPALIVE void SingleStep(VB *sim0, VB *sim1) {
|
||||
uint32_t clocks = 400000; // 1/50s
|
||||
VB *emu = &sims[sim];
|
||||
SingleStepPC = vbGetProgramCounter(emu, VB_PC);
|
||||
emu->onFetch = &SingleStepProc;
|
||||
vbEmulate(emu, NULL, &clocks);
|
||||
emu->onFetch = NULL;
|
||||
VB *sims[2];
|
||||
|
||||
SingleStepPC = vbGetProgramCounter(sim0);
|
||||
vbSetCallback(sim0, VB_ONFETCH, &SingleStepProc);
|
||||
|
||||
if (sim1 != NULL) {
|
||||
sims[0] = sim0;
|
||||
sims[1] = sim1;
|
||||
vbEmulateMulti(sims, 2, &clocks);
|
||||
}
|
||||
|
||||
else vbEmulate(sim0, &clocks);
|
||||
|
||||
vbSetCallback(sim0, VB_ONFETCH, NULL);
|
||||
}
|
||||
|
||||
// Attempt to execute until the following instruction
|
||||
static uint32_t RunNextPC;
|
||||
static int RunNextProcB(VB *emu, int fetch, VB_ACCESS *acc) {
|
||||
if (fetch == 0 && vbGetProgramCounter(emu, VB_PC) == RunNextPC)
|
||||
return 1;
|
||||
acc->value = vbRead(emu, acc->address, acc->type, 0);
|
||||
return 0;
|
||||
}
|
||||
static int RunNextProcA(VB *emu, VB_INSTRUCTION *inst) {
|
||||
RunNextPC = vbGetProgramCounter(emu, VB_PC) + inst->size;
|
||||
emu->onExecute = NULL;
|
||||
emu->onFetch = &RunNextProcB;
|
||||
return 0;
|
||||
}
|
||||
EMSCRIPTEN_KEEPALIVE void RunNext(int sim) {
|
||||
uint32_t clocks = 400000; // 1/50s
|
||||
VB *emu = &sims[sim];
|
||||
emu->onExecute = &RunNextProcA;
|
||||
vbEmulate(emu, NULL, &clocks);
|
||||
emu->onFetch = NULL;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////// Core Commands ////////////////////////////////
|
||||
|
||||
// Retrieve the value of PC
|
||||
EMSCRIPTEN_KEEPALIVE uint32_t GetProgramCounter(int sim, int type) {
|
||||
return vbGetProgramCounter(&sims[sim], type);
|
||||
}
|
||||
|
||||
// Retrieve the value of a program register
|
||||
EMSCRIPTEN_KEEPALIVE int32_t GetProgramRegister(int sim, int id) {
|
||||
return vbGetProgramRegister(&sims[sim], id);
|
||||
}
|
||||
|
||||
// Retrieve the value of a system register
|
||||
EMSCRIPTEN_KEEPALIVE uint32_t GetSystemRegister(int sim, int id) {
|
||||
return vbGetSystemRegister(&sims[sim], id);
|
||||
}
|
||||
|
||||
// Prepare simulation state instances for use
|
||||
EMSCRIPTEN_KEEPALIVE void Init() {
|
||||
vbInit(&sims[0]);
|
||||
vbInit(&sims[1]);
|
||||
}
|
||||
|
||||
// Read a data unit from the bus
|
||||
EMSCRIPTEN_KEEPALIVE int32_t Read(int sim,uint32_t address,int type,int debug){
|
||||
return vbRead(&sims[sim], address, type, debug);
|
||||
}
|
||||
|
||||
// Simulate a hardware reset
|
||||
EMSCRIPTEN_KEEPALIVE void Reset(int sim) {
|
||||
vbReset(&sims[sim]);
|
||||
}
|
||||
|
||||
// Specify a new value for PC
|
||||
EMSCRIPTEN_KEEPALIVE uint32_t SetProgramCounter(int sim, uint32_t value) {
|
||||
return vbSetProgramCounter(&sims[sim], value);
|
||||
}
|
||||
|
||||
// Specify a new value for a program register
|
||||
EMSCRIPTEN_KEEPALIVE int32_t SetProgramRegister(int sim,int id,int32_t value) {
|
||||
return vbSetProgramRegister(&sims[sim], id, value);
|
||||
}
|
||||
|
||||
// Supply a ROM buffer
|
||||
EMSCRIPTEN_KEEPALIVE int SetROM(int sim, void *rom, uint32_t size) {
|
||||
free(vbGetROM(&sims[sim], NULL));
|
||||
return vbSetROM(&sims[sim], rom, size);
|
||||
}
|
||||
|
||||
// Supply an SRAM buffer
|
||||
EMSCRIPTEN_KEEPALIVE int SetSRAM(int sim, void *sram, uint32_t size) {
|
||||
free(vbGetSRAM(&sims[sim], NULL));
|
||||
return vbSetSRAM(&sims[sim], sram, size);
|
||||
}
|
||||
|
||||
// Specify a new value for a system register
|
||||
EMSCRIPTEN_KEEPALIVE uint32_t SetSystemRegister(int sim,int id,uint32_t value){
|
||||
return vbSetSystemRegister(&sims[sim], id, value);
|
||||
}
|
||||
|
||||
// Write a data unit to the bus
|
||||
EMSCRIPTEN_KEEPALIVE void Write(
|
||||
int sim, uint32_t address, int type, int32_t value, int debug) {
|
||||
vbWrite(&sims[sim], address, type, value, debug);
|
||||
EMSCRIPTEN_KEEPALIVE uint32_t Clocks(VB *sim) {
|
||||
return sim->cpu.clocks;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue