pvbemu/app/windows/MemoryWindow.js

307 lines
9.3 KiB
JavaScript

"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.pending = { mode: null };
this.rows = [];
// 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.hex)));
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,
debug : "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;
}
}
// 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(address, 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 /////////////////////////////
// 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].address.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);
}
// Key down event handler
onkeyhex(e) {
// Control is pressed
if (e.ctrlKey) switch (e.key) {
case "g": case "G":
let addr = prompt(this.application.translate("{app.goto_}"));
if (addr === null)
break;
this.seek(
(parseInt(addr, 16) & 0xFFFFFFF0) >>> 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();
}
// 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.hex));
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);
}
}
};
// One row of output
MemoryWindow.Row = class Row extends Toolkit.Panel {
// Object constructor
constructor(parent) {
super(parent.application, {
layout : "grid",
columns : "repeat(17, max-content)",
hollow : false,
overflowX: "visible",
overflowY: "visible"
});
// Configure instance fields
this.bytes = new Array(16);
// Configure element
this.element.setAttribute("role", "row");
// Address label
this.address = this.add(parent.newLabel({ text: "\u00a0" }));
this.address.element.setAttribute("role", "gridcell");
this.address.element.setAttribute("name", "address");
// Byte labels
for (let x = 0; x < 16; x++) {
let lbl = this.bytes[x] =
this.add(parent.newLabel({ text: "\u00a0" }));
lbl.element.setAttribute("role", "gridcell");
lbl.element.setAttribute("name", "byte");
}
}
///////////////////////////// Package Methods /////////////////////////////
// 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));
}
};