482 lines
14 KiB
JavaScript
482 lines
14 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.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(); 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",
|
|
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 "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");
|
|
|
|
// 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;
|
|
}
|
|
|
|
};
|
|
|
|
// 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");
|
|
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
|
|
|
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;
|
|
}
|
|
|
|
// Pointer down event handler
|
|
onpointerdown(e) {
|
|
if (e.button == 0)
|
|
this.wnd.setSelected(this.address());
|
|
}
|
|
|
|
};
|