Adding Memory window
This commit is contained in:
+84
-26
@@ -3,8 +3,49 @@
|
||||
// Top-level state and UI manager
|
||||
globalThis.App = class App {
|
||||
|
||||
// Object constructor
|
||||
constructor() {
|
||||
// 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();
|
||||
@@ -29,6 +70,10 @@ globalThis.App = class App {
|
||||
// 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._}" });
|
||||
@@ -40,27 +85,41 @@ globalThis.App = class App {
|
||||
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));
|
||||
|
||||
// Theme menu
|
||||
menu = this.mainMenu.newMenu({ text: "{menu.theme._}"});
|
||||
item = menu.newMenuItem({ text: "{menu.theme.light}"});
|
||||
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 = menu.newMenuItem({ text: "{menu.theme.dark}" });
|
||||
item.addClickListener(()=>this.setTheme("dark"));
|
||||
item = menu.newMenuItem({ text: "{menu.theme.virtual}"});
|
||||
item = menu.newMenuItem({ text: "{menu.theme.virtual}" });
|
||||
item.addClickListener(()=>this.setTheme("virtual"));
|
||||
}
|
||||
|
||||
// Desktop pane
|
||||
let desktop = this.gui.newPanel({ layout: "desktop" });
|
||||
desktop.setRole("group");
|
||||
desktop.element.setAttribute("desktop", "");
|
||||
this.gui.add(desktop);
|
||||
|
||||
let wnd = this.gui.newWindow({
|
||||
title: "{memory._}"
|
||||
});
|
||||
desktop.add(wnd);
|
||||
wnd.setLocation ( 20, 10);
|
||||
wnd.setClientSize(384, 224);
|
||||
///////////////////////////// Message Methods /////////////////////////////
|
||||
|
||||
// Message received
|
||||
onmessage(msg) {
|
||||
if ("debug" 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.memory.setVisible(true, true);
|
||||
dbg.refresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -71,12 +130,12 @@ globalThis.App = class App {
|
||||
loadROM() {
|
||||
let file = document.createElement("input");
|
||||
file.type = "file";
|
||||
file.addEventListener("input", ()=>this.setROM(file.files[0]));
|
||||
file.addEventListener("input", ()=>this.selectROM(file.files[0]));
|
||||
file.click();
|
||||
}
|
||||
|
||||
// Specify a ROM file
|
||||
async setROM(file) {
|
||||
async selectROM(file) {
|
||||
|
||||
// No file is specified (perhaps the user canceled)
|
||||
if (file == null)
|
||||
@@ -94,18 +153,17 @@ globalThis.App = class App {
|
||||
|
||||
// Load the file data into a byte buffer
|
||||
let filename = file.name;
|
||||
try { file = new Uint8Array(await file.arrayBuffer()); }
|
||||
try { file = await file.arrayBuffer(); }
|
||||
catch {
|
||||
alert(this.gui.translate("{app.readFileError}"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Testing output pending further features
|
||||
alert(this.gui.translate("{app.romLoaded}", {
|
||||
filename: filename,
|
||||
size : file.length + " byte" + (file.length == 1 ? "" : "s")
|
||||
}));
|
||||
|
||||
this.core.postMessage({
|
||||
command: "SetROM",
|
||||
rom : file,
|
||||
sim : 0
|
||||
});
|
||||
}
|
||||
|
||||
// Specify the current color theme
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"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;
|
||||
|
||||
// Configure Memory window
|
||||
this.memory = new MemoryWindow(this, {
|
||||
title : "{sim}{memory._}",
|
||||
center : true,
|
||||
height : 300,
|
||||
visible: false,
|
||||
width : 400
|
||||
});
|
||||
this.memory.addCloseListener(e=>this.memory.setVisible(false));
|
||||
app.desktop.add(this.memory);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Message received from emulation thread
|
||||
message(msg) {
|
||||
switch (msg.debug) {
|
||||
case "Memory": this.memory.message(msg); break;
|
||||
}
|
||||
}
|
||||
|
||||
// Reload all output
|
||||
refresh() {
|
||||
this.memory.refresh();
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
"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 message port
|
||||
onmessage = e=>this.onmessage(e.data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Message Methods /////////////////////////////
|
||||
|
||||
// Message received
|
||||
onmessage(msg) {
|
||||
switch (msg.command) {
|
||||
case "Init" : this.init (msg); break;
|
||||
case "ReadBuffer": this.readBuffer(msg); break;
|
||||
case "SetROM" : this.setROM (msg); break;
|
||||
}
|
||||
}
|
||||
|
||||
// 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: function(){} }});
|
||||
this.wasm.instance.exports.Init();
|
||||
|
||||
// Configure instance fields
|
||||
this.core = this.wasm.instance.exports;
|
||||
this.memory = this.core.memory.buffer;
|
||||
|
||||
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.buffer = buffer.buffer.slice(
|
||||
buffer.pointer, buffer.pointer + msg.size);
|
||||
this.core.Free(buffer.pointer);
|
||||
postMessage(msg, msg.buffer);
|
||||
}
|
||||
|
||||
// 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[x] = rom[x];
|
||||
msg.success = !!this.core.SetROM(msg.sim, buffer.pointer, rom.length);
|
||||
delete msg.rom;
|
||||
postMessage(msg);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Utility Methods /////////////////////////////
|
||||
|
||||
// Allocate a typed array in WebAssembly core memory
|
||||
malloc(type, size) {
|
||||
let pointer = this.core.Malloc(size);
|
||||
let ret = new type(this.memory, pointer, size);
|
||||
ret.pointer = pointer;
|
||||
return ret;
|
||||
}
|
||||
|
||||
}).initializer();
|
||||
+4
-1
@@ -285,15 +285,18 @@ for (let file of manifest) {
|
||||
// Program startup
|
||||
let run = async function() {
|
||||
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/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/Window.js");
|
||||
new App();
|
||||
await Bundle.run("app/windows/MemoryWindow.js");
|
||||
await App.create();
|
||||
};
|
||||
run();
|
||||
|
||||
+6
-2
@@ -4,6 +4,7 @@
|
||||
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."
|
||||
@@ -12,8 +13,11 @@
|
||||
_: "Memory"
|
||||
},
|
||||
menu: {
|
||||
_ : "Main application menu",
|
||||
file: {
|
||||
_ : "Main application menu",
|
||||
debug: {
|
||||
_: "Debug",
|
||||
},
|
||||
file : {
|
||||
_ : "File",
|
||||
loadROM: "Load ROM..."
|
||||
},
|
||||
|
||||
+4
-2
@@ -2,11 +2,13 @@
|
||||
--control : #222222;
|
||||
--control-focus : #444444;
|
||||
--control-shadow : #999999;
|
||||
--control-text : #cccccc;
|
||||
--desktop : #111111;
|
||||
--text : #cccccc;
|
||||
--window-blur : #555555;
|
||||
--window-blur-text : #cccccc;
|
||||
--window-close-text: #cccccc;
|
||||
--window-close : #ee9999;
|
||||
--window-close-blur: #998080;
|
||||
--window-close-text: #ffffff;
|
||||
--window-focus : #007ACC;
|
||||
--window-focus-text: #ffffff;
|
||||
}
|
||||
+62
-28
@@ -1,9 +1,15 @@
|
||||
/* Common styles for all themes */
|
||||
|
||||
:root {
|
||||
color : var(--text);
|
||||
font-family: Arial, sans-serif;
|
||||
font-size : 12px;
|
||||
--font-dialog: Arial, sans-serif;
|
||||
--font-hex : Consolas, monospace;
|
||||
--font-size : 12;
|
||||
}
|
||||
|
||||
:root {
|
||||
color : var(--control-text);
|
||||
font-family: var(--font-dialog);
|
||||
font-size : calc(1px * var(--font-size));
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -47,13 +53,14 @@ body {
|
||||
|
||||
[role="menubar"] {
|
||||
background : var(--control);
|
||||
border-bottom: 1px solid var(--text);
|
||||
border-bottom: 1px solid var(--control-text);
|
||||
padding : 2px 3px 3px 2px;
|
||||
}
|
||||
|
||||
[role="menubar"] [role="menuitem"] {
|
||||
margin : 1px;
|
||||
padding: 3px;
|
||||
line-height: 1em;
|
||||
margin : 1px;
|
||||
padding : 3px;
|
||||
}
|
||||
|
||||
[role="menubar"] [role="menuitem"]:focus {
|
||||
@@ -77,8 +84,8 @@ body {
|
||||
[role="menubar"] [role="menu"] {
|
||||
background: var(--control);
|
||||
box-shadow:
|
||||
0 0 0 1px var(--text),
|
||||
1px 1px 0 1px var(--text)
|
||||
0 0 0 1px var(--control-text),
|
||||
1px 1px 0 1px var(--control-text)
|
||||
;
|
||||
min-height: 16px;
|
||||
min-width : 16px;
|
||||
@@ -97,13 +104,14 @@ body {
|
||||
background: var(--control);
|
||||
box-shadow:
|
||||
0 0 0 1px var(--control),
|
||||
0 0 0 2px var(--text),
|
||||
1px 1px 0 2px var(--text)
|
||||
0 0 0 2px var(--control-text),
|
||||
1px 1px 0 2px var(--control-text)
|
||||
;
|
||||
row-gap : 3px;
|
||||
}
|
||||
|
||||
[role="dialog"] [name="title-bar"] {
|
||||
min-height: calc(1em + 5px);
|
||||
}
|
||||
|
||||
[role="dialog"][focus="true"] [name="title-bar"] {
|
||||
@@ -125,13 +133,14 @@ body {
|
||||
[role="dialog"] [name="title-icon"],
|
||||
[role="dialog"] [name="title-close-box"] {
|
||||
align-self : stretch;
|
||||
min-width : calc(1em + 4px);
|
||||
width : calc(1em + 4px);
|
||||
min-width : calc(1em + 5px);
|
||||
width : calc(1em + 5px);
|
||||
}
|
||||
|
||||
[role="dialog"] [name="title"] {
|
||||
color : var(--window-focus-text);
|
||||
font-weight : bold;
|
||||
line-height : calc(1em + 1px);
|
||||
overflow : hidden;
|
||||
padding : 2px;
|
||||
text-align : center;
|
||||
@@ -140,28 +149,29 @@ body {
|
||||
}
|
||||
|
||||
[role="dialog"][focus="false"] [name="title"] {
|
||||
color : var(--window-blur-text);
|
||||
color: var(--window-blur-text);
|
||||
}
|
||||
|
||||
[role="dialog"] [name="title-close-box"] {
|
||||
align-items : center;
|
||||
display : flex;
|
||||
justify-content: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[role="dialog"] [name="title-close"] {
|
||||
align-items : center;
|
||||
background : var(--control);
|
||||
box-shadow :
|
||||
0 0 0 1px var(--control),
|
||||
0 0 0 2px var(--control-shadow)
|
||||
;
|
||||
background : var(--window-close);
|
||||
box-shadow : 0 0 0 1px var(--control-shadow);
|
||||
display : flex;
|
||||
height : 11px;
|
||||
height : 13px;
|
||||
justify-content: center;
|
||||
overflow : hidden;
|
||||
padding : 0;
|
||||
width : 11px;
|
||||
width : 13px;
|
||||
}
|
||||
|
||||
[role="dialog"][focus="false"] [name="title-close"] {
|
||||
background: var(--window-close-blur);
|
||||
}
|
||||
|
||||
[role="dialog"] [name="title-close"]:focus {
|
||||
@@ -169,20 +179,44 @@ body {
|
||||
}
|
||||
|
||||
[role="dialog"] [name="title-close"][active] {
|
||||
box-shadow :
|
||||
-1px -1px 0 1px var(--control),
|
||||
-1px -1px 0 2px var(--control-shadow)
|
||||
;
|
||||
margin: 2px 0 0 2px;
|
||||
box-shadow: 0 0 0 1px var(--control-shadow);
|
||||
margin : 0;
|
||||
}
|
||||
|
||||
[role="dialog"] [name="title-close"]:after {
|
||||
color : var(--window-close-text);
|
||||
content : '\00d7';
|
||||
font-size : 12px;
|
||||
line-height: 1em;
|
||||
font-size : 13px;
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
[role="dialog"] [name="title-close"][active]:after {
|
||||
color : var(--window-close-text);
|
||||
content : '\00d7';
|
||||
font-size : 13px;
|
||||
line-height: 13px;
|
||||
margin : 1px -1px -1px 1px;
|
||||
}
|
||||
|
||||
[role="dialog"] [name="client"] {
|
||||
background: var(--control);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/******************************* Memory Window *******************************/
|
||||
|
||||
[window="memory"] [name="client"] {
|
||||
align-items: center;
|
||||
column-gap : calc(1px * var(--font-size) / 2);
|
||||
}
|
||||
|
||||
[window="memory"] [name="address"],
|
||||
[window="memory"] [name="byte"] {
|
||||
font-family: var(--font-hex);
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
[window="memory"] [name="address"] {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
+4
-2
@@ -2,11 +2,13 @@
|
||||
--control : #eeeeee;
|
||||
--control-focus : #cccccc;
|
||||
--control-shadow : #999999;
|
||||
--control-text : #000000;
|
||||
--desktop : #cccccc;
|
||||
--text : #000000;
|
||||
--window-blur : #cccccc;
|
||||
--window-blur-text : #444444;
|
||||
--window-close-text: #444444;
|
||||
--window-close : #ee9999;
|
||||
--window-close-blur: #d4c4c4;
|
||||
--window-close-text: #ffffff;
|
||||
--window-focus : #80ccff;
|
||||
--window-focus-text: #000000;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
--control : #000000;
|
||||
--control-focus : #550000;
|
||||
--control-shadow : #aa0000;
|
||||
--control-text : #ff0000;
|
||||
--desktop : #000000;
|
||||
--text : #ff0000;
|
||||
--window-blur : #000000;
|
||||
--window-blur-text : #aa0000;
|
||||
--window-close : #aa0000;
|
||||
--window-close-blur: #550000;
|
||||
--window-close-text: #ff0000;
|
||||
--window-focus : #550000;
|
||||
--window-focus-text: #ff0000;
|
||||
|
||||
@@ -39,11 +39,6 @@ Toolkit.Component = class Component {
|
||||
this.resizeListeners.push(listener);
|
||||
}
|
||||
|
||||
// Remove the component from its parent
|
||||
remove() {
|
||||
this.parent && this.parent.remove(this);
|
||||
}
|
||||
|
||||
// Retrieve the bounding box of the element
|
||||
getBounds() {
|
||||
return this.element.getBoundingClientRect();
|
||||
@@ -62,6 +57,14 @@ Toolkit.Component = class Component {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Specify the location and size of the component
|
||||
setBounds(left, top, width, height) {
|
||||
this.setLeft (left );
|
||||
this.setTop (top );
|
||||
this.setWidth (width );
|
||||
this.setHeight(height);
|
||||
}
|
||||
|
||||
// Specify the display CSS property of the visible element
|
||||
setDisplay(display) {
|
||||
this.display = display || null;
|
||||
@@ -90,6 +93,12 @@ Toolkit.Component = class Component {
|
||||
this.setTop (top );
|
||||
}
|
||||
|
||||
// Specify a localization property value
|
||||
setProperty(key, value) {
|
||||
this.properties[key] = value;
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Specify both the width and the height of the component
|
||||
setSize(width, height) {
|
||||
this.setHeight(height);
|
||||
@@ -146,7 +155,7 @@ Toolkit.Component = class Component {
|
||||
onresized() {
|
||||
let bounds = this.getBounds();
|
||||
for (let listener of this.resizeListeners)
|
||||
listener(bounds, this);
|
||||
listener(bounds);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"use strict";
|
||||
|
||||
// Display text component
|
||||
Toolkit.Label = class Label extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, "div", options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
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.setText(this.text);
|
||||
if (this.localized)
|
||||
this.application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Retrieve the label's display text
|
||||
getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
// Specify the label's display text
|
||||
setText(text) {
|
||||
this.text = text || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// 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;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -17,6 +17,9 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem {
|
||||
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;
|
||||
|
||||
@@ -213,4 +216,10 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Prevent an event from bubbling
|
||||
stopEvent(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
+24
-8
@@ -6,9 +6,9 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, "div", options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
options = options || {};
|
||||
this.alignCross = "start";
|
||||
this.alignMain = "start";
|
||||
this.application = application;
|
||||
@@ -22,8 +22,10 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
|
||||
this.wrap = false;
|
||||
|
||||
// Configure element
|
||||
this.element.style.minHeight = "0";
|
||||
this.element.style.minWidth = "0";
|
||||
if ("noShrink" in options ? !options.noShrink : true) {
|
||||
this.element.style.minHeight = "0";
|
||||
this.element.style.minWidth = "0";
|
||||
}
|
||||
this.setOverflow(this.overflowX, this.overflowY);
|
||||
|
||||
// Configure layout
|
||||
@@ -57,6 +59,11 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
|
||||
return new Toolkit.Button(this.application, options);
|
||||
}
|
||||
|
||||
// Create a Label and associate it with the application
|
||||
newLabel(options) {
|
||||
return new Toolkit.Label(this.application, options);
|
||||
}
|
||||
|
||||
// Create a MenuBar and associate it with the application
|
||||
newMenuBar(options) {
|
||||
return new Toolkit.MenuBar(this.application, options);
|
||||
@@ -141,6 +148,16 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Move a window to the foreground
|
||||
bringToFront(wnd) {
|
||||
for (let child of this.children)
|
||||
child.element.style.zIndex = child == wnd ? "0" : "1";
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Resize event handler
|
||||
@@ -152,12 +169,13 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
|
||||
|
||||
// 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(
|
||||
desktop,
|
||||
bounds,
|
||||
bounds.x - desktop.x,
|
||||
bounds.y - desktop.y
|
||||
bounds.y - desktop.y,
|
||||
desktop, bounds
|
||||
);
|
||||
}
|
||||
|
||||
@@ -244,8 +262,6 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
|
||||
this.element.style.removeProperty("grid-template-rows" );
|
||||
this.element.style.removeProperty("justify-content" );
|
||||
this.element.style.removeProperty("flex-direction" );
|
||||
this.element.style.removeProperty("overflow-x" );
|
||||
this.element.style.removeProperty("overflow-y" );
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
+74
-13
@@ -9,11 +9,16 @@ Toolkit.Window = class Window extends Toolkit.Panel {
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.dragBounds = null;
|
||||
this.dragCursor = { x: 0, y: 0 };
|
||||
this.dragEdge = null;
|
||||
this.lastFocus = this.element;
|
||||
this.title = options.title || "";
|
||||
this.closeListeners = [];
|
||||
this.dragBounds = null;
|
||||
this.dragCursor = { x: 0, y: 0 };
|
||||
this.dragEdge = null;
|
||||
this.initialCenter = "center" in options ? !!options.center : false;
|
||||
this.initialHeight = options.height || 64;
|
||||
this.initialWidth = options.width || 64;
|
||||
this.lastFocus = this.element;
|
||||
this.shown = this.visible;
|
||||
this.title = options.title || "";
|
||||
|
||||
// Configure element
|
||||
this.setLayout("flex", {
|
||||
@@ -23,6 +28,7 @@ Toolkit.Window = class Window extends Toolkit.Panel {
|
||||
overflowY : "visible"
|
||||
});
|
||||
this.setRole("dialog");
|
||||
this.setBounds(0, 0, 64, 64);
|
||||
this.element.style.position = "absolute";
|
||||
this.element.setAttribute("aria-modal", "false");
|
||||
this.element.setAttribute("focus" , "false");
|
||||
@@ -53,6 +59,7 @@ Toolkit.Window = class Window extends Toolkit.Panel {
|
||||
layout : "flex",
|
||||
alignCross: "center",
|
||||
direction : "row",
|
||||
noShrink : true,
|
||||
overflowX : "visible",
|
||||
overflowY : "visible"
|
||||
}));
|
||||
@@ -64,7 +71,7 @@ Toolkit.Window = class Window extends Toolkit.Panel {
|
||||
this.titleIcon.element.style.removeProperty("min-width");
|
||||
|
||||
// Configure title text element
|
||||
this.titleElement = this.titleBar.add(this.newPanel({}));
|
||||
this.titleElement = this.titleBar.add(this.newLabel({}));
|
||||
this.titleElement.element.setAttribute("name", "title");
|
||||
this.titleElement.element.style.cursor = "default";
|
||||
this.titleElement.element.style.flexGrow = "1";
|
||||
@@ -81,9 +88,13 @@ Toolkit.Window = class Window extends Toolkit.Panel {
|
||||
toolTip : "{app.close}"
|
||||
}));
|
||||
this.titleClose.element.setAttribute("name", "title-close");
|
||||
this.titleClose.addClickListener(e=>this.onclose(e));
|
||||
|
||||
// Configure client area
|
||||
this.client = this.body.add(this.newPanel({}));
|
||||
this.client = this.body.add(this.newPanel({
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden"
|
||||
}));
|
||||
this.client.element.style.flexGrow = "1";
|
||||
this.client.element.setAttribute("name", "client");
|
||||
this.client.element.addEventListener(
|
||||
@@ -91,6 +102,8 @@ Toolkit.Window = class Window extends Toolkit.Panel {
|
||||
|
||||
// Configure properties
|
||||
this.setTitle(this.title);
|
||||
if (this.shown)
|
||||
this.setClientSize(this.initialHeight, this.initialWidth);
|
||||
application.addComponent(this);
|
||||
}
|
||||
|
||||
@@ -98,6 +111,12 @@ Toolkit.Window = class Window extends Toolkit.Panel {
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a callback for close events
|
||||
addCloseListener(listener) {
|
||||
if (this.closeListeners.indexOf(listener) == -1)
|
||||
this.closeListeners.push(listener);
|
||||
}
|
||||
|
||||
// Specify the size of the client rectangle in pixels
|
||||
setClientSize(width, height) {
|
||||
let bounds = this.getBounds();
|
||||
@@ -114,6 +133,19 @@ Toolkit.Window = class Window extends Toolkit.Panel {
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Specify whether the component is visible
|
||||
setVisible(visible, focus) {
|
||||
super.setVisible(visible);
|
||||
if (this.client === undefined)
|
||||
return;
|
||||
if (visible)
|
||||
this.contain();
|
||||
if (focus)
|
||||
this.focus();
|
||||
if (!this.shown)
|
||||
this.firstShow();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
@@ -123,6 +155,7 @@ Toolkit.Window = class Window extends Toolkit.Panel {
|
||||
if (this.lastFocus != this)
|
||||
this.lastFocus.focus();
|
||||
else this.element.focus();
|
||||
this.parent.bringToFront(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -130,9 +163,14 @@ Toolkit.Window = class Window extends Toolkit.Panel {
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Position the window using a tentative location in the desktop
|
||||
contain(desktop, bounds, x, y, client) {
|
||||
bounds = bounds || this.getBounds();
|
||||
client = client || this.client.getBounds();
|
||||
contain(x, y, desktop, bounds, client) {
|
||||
desktop = desktop || this.parent.getBounds();
|
||||
bounds = bounds || this.getBounds();
|
||||
client = client || this.client.getBounds();
|
||||
if (x === undefined)
|
||||
x = bounds.x - desktop.x;
|
||||
if (y === undefined)
|
||||
y = bounds.y - desktop.y;
|
||||
|
||||
// Restrict window position
|
||||
x = Math.min(x, desktop.width - 16);
|
||||
@@ -172,6 +210,25 @@ Toolkit.Window = class Window extends Toolkit.Panel {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The window is being displayed for the first time
|
||||
firstShow() {
|
||||
this.shown = true;
|
||||
|
||||
// Configure the initial size of the window
|
||||
this.setClientSize(this.initialWidth, this.initialHeight);
|
||||
|
||||
// Configure the initial position of the window
|
||||
if (!this.initialCenter)
|
||||
return;
|
||||
let bounds = this.getBounds();
|
||||
let desktop = this.parent.getBounds();
|
||||
this.contain(
|
||||
Math.floor((desktop.width - bounds.width ) / 2),
|
||||
Math.ceil ((desktop.height - bounds.height) / 2),
|
||||
desktop, bounds
|
||||
);
|
||||
}
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let title = this.title;
|
||||
@@ -193,6 +250,12 @@ Toolkit.Window = class Window extends Toolkit.Panel {
|
||||
this.focus();
|
||||
}
|
||||
|
||||
// Window close
|
||||
onclose(e) {
|
||||
for (let listener of this.closeListeners)
|
||||
listener(e);
|
||||
}
|
||||
|
||||
// Focus gained event capture
|
||||
onfocus(e) {
|
||||
|
||||
@@ -284,11 +347,9 @@ Toolkit.Window = class Window extends Toolkit.Panel {
|
||||
// Move the window
|
||||
if (this.dragEdge == null) {
|
||||
this.contain(
|
||||
desktop,
|
||||
bounds,
|
||||
this.dragBounds.x - desktop.x + rX,
|
||||
this.dragBounds.y - desktop.y + rY,
|
||||
client
|
||||
desktop, bounds, client
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
"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 = 0xFFFFFFF0;
|
||||
this.debug = debug;
|
||||
this.rows = [new MemoryWindow.Row(this.client)];
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("window", "memory");
|
||||
|
||||
// Configure client
|
||||
this.client.setLayout("grid", { columns: "repeat(19, max-content)" });
|
||||
this.client.element.style.gridAutoRows = "max-content";
|
||||
this.client.setOverflow("auto", "hidden");
|
||||
this.client.addResizeListener(b=>this.refresh(b.height));
|
||||
this.client.element.addEventListener("keydown", e=>this.onkeydown(e));
|
||||
this.client.element.addEventListener("wheel", e=>this.onwheel(e));
|
||||
|
||||
// Configure properties
|
||||
this.setProperty("sim", "");
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// The window is being displayed for the first time
|
||||
firstShow() {
|
||||
super.firstShow();
|
||||
this.seek(this.address, Math.floor(this.lines(true) / 3));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update the display with current emulation data
|
||||
refresh(clientHeight, lineHeight) {
|
||||
clientHeight = clientHeight || this.client.getBounds().height;
|
||||
lineHeight = lineHeight || this.lineHeight();
|
||||
let rowCount = this.lines(false, clientHeight, lineHeight);
|
||||
|
||||
// Showing for the first time
|
||||
if (this.address < 0)
|
||||
this.seek(-this.address, Math.floor(rowCount / 3));
|
||||
|
||||
// Request bus data from the WebAssembly core
|
||||
this.debug.core.postMessage({
|
||||
command: "ReadBuffer",
|
||||
debug : "Memory",
|
||||
address: this.address,
|
||||
sim : this.debug.sim,
|
||||
size : rowCount * 16
|
||||
});
|
||||
|
||||
// Configure elements
|
||||
for (let y = this.rows.length; y < rowCount; y++)
|
||||
this.rows[y] = new MemoryWindow.Row(this.client);
|
||||
for (let y = rowCount; y < this.rows.length; y++)
|
||||
this.rows[y].remove();
|
||||
if (this.rows.length > rowCount)
|
||||
this.rows.splice(rowCount, this.rows.length - rowCount);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Message Methods /////////////////////////////
|
||||
|
||||
// Message received
|
||||
message(msg) {
|
||||
switch (msg.command) {
|
||||
case "ReadBuffer": this.readBuffer(msg); break;
|
||||
}
|
||||
}
|
||||
|
||||
// Received bytes from the bus
|
||||
readBuffer(msg) {
|
||||
let bytes = new Uint8Array(msg.buffer);
|
||||
for (
|
||||
let x = 0, address = this.address, offset = 0;
|
||||
x < this.rows.length && offset < bytes.length;
|
||||
x++, address = (address + 16 & 0xFFFFFFFF) >>> 0, offset += 16
|
||||
) this.rows[x].update(address, bytes, offset);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Determine the height in pixels of one row of output
|
||||
lineHeight() {
|
||||
let ret = this.rows[0].address.getBounds().height;
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Determine the number of rows of output
|
||||
lines(fullyVisible, clientHeight, lineHeight) {
|
||||
clientHeight = clientHeight || this.client.getBounds().height;
|
||||
lineHeight = lineHeight || this.lineHeight();
|
||||
let ret = clientHeight / lineHeight;
|
||||
ret = fullyVisible ? Math.floor(ret) : Math.ceil(ret);
|
||||
return Math.max(1, Math.min(ret, Math.floor(clientHeight / 8)));
|
||||
}
|
||||
|
||||
// Key down event handler
|
||||
onkeydown(e) {
|
||||
|
||||
// Control is pressed
|
||||
if (e.ctrlKey) switch (e.key) {
|
||||
case "g": case "G":
|
||||
let addr = prompt(this.application.translate("{app.romLoaded}"));
|
||||
if (addr === null)
|
||||
break;
|
||||
this.seek(
|
||||
(parseInt(addr, 16) & 0xFFFFFFF0) >>> 0,
|
||||
Math.floor(this.lines(true) / 3)
|
||||
);
|
||||
this.refresh();
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Processing by key
|
||||
else switch (e.key) {
|
||||
|
||||
case "ArrowDown":
|
||||
this.address = (this.address + 16 & 0xFFFFFFFF) >>> 0;
|
||||
this.refresh();
|
||||
break;
|
||||
|
||||
case "ArrowUp":
|
||||
this.address = (this.address - 16 & 0xFFFFFFFF) >>> 0;
|
||||
this.refresh();
|
||||
break;
|
||||
|
||||
case "PageUp":
|
||||
this.address = (this.address - 16 * this.lines(true) &
|
||||
0xFFFFFFFF) >>> 0;
|
||||
this.refresh();
|
||||
break;
|
||||
|
||||
case "PageDown":
|
||||
this.address = (this.address + 16 * this.lines(true) &
|
||||
0xFFFFFFFF) >>> 0;
|
||||
this.refresh();
|
||||
break;
|
||||
|
||||
default: console.log(e.key); return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Mouse wheel event handler
|
||||
onwheel(e) {
|
||||
let lineHeight = this.lineHeight();
|
||||
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 / lineHeight));
|
||||
this.address = (this.address + sign * mag * 16 & 0xFFFFFFFF) >>> 0;
|
||||
this.refresh(null, lineHeight);
|
||||
}
|
||||
|
||||
// Move to a new address positioned at a particular row of output
|
||||
seek(address, index) {
|
||||
this.address = (address - (index || 0) * 16 & 0xFFFFFFFF) >>> 0;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// One row of output
|
||||
MemoryWindow.Row = class Row {
|
||||
|
||||
// Object constructor
|
||||
constructor(client) {
|
||||
|
||||
// Configure instance fields
|
||||
this.bytes = new Array(16);
|
||||
this.client = client;
|
||||
this.spacers = new Array(2);
|
||||
|
||||
// Address label
|
||||
this.address = client.add(client.newLabel({ text: "\u00a0" }));
|
||||
this.address.element.setAttribute("name", "address");
|
||||
|
||||
// Byte labels
|
||||
for (let x = 0; x < 16; x++) {
|
||||
if ((x & 7) == 0) {
|
||||
let lbl = this.spacers[x / 8] =
|
||||
client.add(client.newLabel({ text: "" }));
|
||||
lbl.element.setAttribute("name", "byte");
|
||||
}
|
||||
let lbl = this.bytes[x] =
|
||||
client.add(client.newLabel({ text: "\u00a0" }));
|
||||
lbl.element.setAttribute("name", "byte");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Remove components from the memory window
|
||||
remove() {
|
||||
this.client.remove(this.address);
|
||||
for (let bytel of this.bytes)
|
||||
this.client.remove(bytel);
|
||||
for (let spacer of this.spacers)
|
||||
this.client.remove(spacer);
|
||||
}
|
||||
|
||||
// Update the output labels with emulation state content
|
||||
update(address, bytes, offset) {
|
||||
this.address.setText(
|
||||
("0000000" + address.toString(16).toUpperCase()).slice(-8));
|
||||
for (let x = 0; x < 16; x++, offset++)
|
||||
this.bytes[x].setText(
|
||||
("0" + bytes[offset].toString(16).toUpperCase()).slice(-2));
|
||||
}
|
||||
|
||||
};
|
||||
Reference in New Issue
Block a user