"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()); } };