import { Toolkit } from /**/"../toolkit/Toolkit.js"; let register = Debugger => Debugger.Memory = // Debugger memory window class Memory extends Toolkit.Window { //////////////////////////////// Constants //////////////////////////////// // Bus indexes static MEMORY = 0; ///////////////////////// Initialization Methods ////////////////////////// constructor(debug, index) { super(debug.app, { class: "tk window memory" }); // Configure instance fields this.data = null, this.dataAddress = null, this.debug = debug; this.delta = 0; this.editDigit = null; this.height = 300; this.index = index; this.lines = []; this.pending = false; this.shown = false; this.subscription = [ 0, index, "memory", "refresh" ]; this.width = 400; // Available buses this.buses = [ { index : Memory.MEMORY, editAddress: 0x05000000, viewAddress: 0x05000000 } ]; this.bus = this.buses[Memory.MEMORY]; // Window this.setTitle("{debug.memory._}", true); this.substitute("#", " " + (index + 1)); if (index == 1) this.element.classList.add("two"); this.addEventListener("close" , e=>this.visible = false); this.addEventListener("visibility", e=>this.onVisibility(e)); // Client area Object.assign(this.client.style, { display : "grid", gridTemplateRows: "max-content auto" }); // Bus drop-down this.drpBus = new Toolkit.DropDown(debug.app); this.drpBus.setLabel("{debug.memory.bus}", true); this.drpBus.setTitle("{debug.memory.bus}", true); this.drpBus.add("{debug.memory.busMemory}", true, this.buses[Memory.MEMORY]); this.drpBus.addEventListener("input", e=>this.busInput()); this.add(this.drpBus); // Hex editor this.hexEditor = new Toolkit.Component(debug.app, { class : "tk mono hex-editor", role : "application", tabIndex: "0", style : { display : "grid", gridTemplateColumns: "repeat(17, max-content)", height : "100%", minWidth : "100%", overflow : "hidden", position : "relative", width : "max-content" } }); this.hexEditor.localize = ()=>{ this.hexEditor.localizeRoleDescription(); this.hexEditor.localizeLabel(); }; this.hexEditor.setLabel("{debug.memory.hexEditor}", true); this.hexEditor.setRoleDescription("{debug.memory.hexEditor}", true); this.hexEditor.addEventListener("keydown", e=>this.hexKeyDown(e)); this.hexEditor.addEventListener("resize" , e=>this.hexResize ( )); this.hexEditor.addEventListener("wheel" , e=>this.hexWheel (e)); this.hexEditor.addEventListener( "pointerdown", e=>this.hexPointerDown(e)); this.lastFocus = this.hexEditor; // Label for measuring text dimensions this.sizer = new Toolkit.Label(debug.app, { class : "tk label mono", visible : false, visibility: true, style: { position: "absolute" } }); this.sizer.setText("\u00a0", false); //   this.hexEditor.append(this.sizer); // Hex editor scroll pane this.scrHex = new Toolkit.ScrollPane(debug.app, { overflowX: "auto", overflowY: "hidden", view : this.hexEditor }); this.add(this.scrHex); // Hide the bus drop-down: Virtual Boy only has one bus this.drpBus.visible = false; this.client.style.gridTemplateRows = "auto"; } ///////////////////////////// Event Handlers ////////////////////////////// // Bus drop-down selection busInput() { // An edit is in progress if (this.editDigit !== null) this.commit(false); // Switch to the new bus this.bus = this.drpBus.value; this.fetch(); } // Hex editor key press hexKeyDown(e) { // Error checking if (e.altKey) return; // Processing by key, Ctrl pressed if (e.ctrlKey) switch (e.key) { case "g": case "G": Toolkit.handle(e); this.goto(); return; default: return; } // Processing by key, scroll lock off if (!e.getModifierState("ScrollLock")) switch (e.key) { case "ArrowDown": this.commit(); this.setEditAddress(this.bus.editAddress + 16); Toolkit.handle(e); return; case "ArrowLeft": this.commit(); this.setEditAddress(this.bus.editAddress - 1); Toolkit.handle(e); return; case "ArrowRight": this.commit(); this.setEditAddress(this.bus.editAddress + 1); Toolkit.handle(e); return; case "ArrowUp": this.commit(); this.setEditAddress(this.bus.editAddress - 16); Toolkit.handle(e); return; case "PageDown": this.commit(); this.setEditAddress(this.bus.editAddress + this.tall(true)*16); Toolkit.handle(e); return; case "PageUp": this.commit(); this.setEditAddress(this.bus.editAddress - this.tall(true)*16); Toolkit.handle(e); return; } // Processing by key, scroll lock on else switch (e.key) { case "ArrowDown": this.bus.viewAddress += 16; this.fetch(); Toolkit.handle(e); return; case "ArrowLeft": this.scrHex.scrollLeft -= this.scrHex.hscroll.unitIncrement; Toolkit.handle(e); return; case "ArrowRight": this.scrHex.scrollLeft += this.scrHex.hscroll.unitIncrement; Toolkit.handle(e); return; case "ArrowUp": this.bus.viewAddress -= 16; this.fetch(); Toolkit.handle(e); return; case "PageDown": this.bus.viewAddress += this.tall(true) * 16; this.fetch(); Toolkit.handle(e); return; case "PageUp": this.bus.viewAddress -= this.tall(true) * 16; this.fetch(); Toolkit.handle(e); return; } // Processing by key, editing switch (e.key) { case "0": case "1": case "2": case "3": case "4": case "5": case "6": case "7": case "8": case "9": case "a": case "A": case "b": case "B": case "c": case "C": case "d": case "D": case "e": case "E": case "f": case "F": let digit = parseInt(e.key, 16); if (this.editDigit === null) { this.editDigit = digit; this.setEditAddress(this.bus.editAddress); } else { this.editDigit = this.editDigit << 4 | digit; this.commit(); this.setEditAddress(this.bus.editAddress + 1); } break; // Commit the current edit case "Enter": if (this.editDigit === null) break; this.commit(); this.setEditAddress(this.bus.editAddress + 1); break; // Cancel the current edit case "Escape": if (this.editDigit === null) return; this.editDigit = null; this.setEditAddress(this.bus.editAddress); break; default: return; } Toolkit.handle(e); } // Hex editor pointer down hexPointerDown(e) { // Error checking if (e.button != 0) return; // Working variables let cols = this.lines[0].lblBytes.map(l=>l.getBoundingClientRect()); let y = Math.max(0, Math.floor((e.clientY-cols[0].y)/cols[0].height)); let x = 15; // Determine which column is closest to the touch point if (e.clientX < cols[15].right) { for (let l = 0; l < 15; l++) { if (e.clientX > (cols[l].right + cols[l + 1].x) / 2) continue; x = l; break; } } // Update the selection address let address = this.bus.viewAddress + y * 16 + x >>> 0; if (this.editDigit !== null && address != this.bus.editAddress) this.commit(); this.setEditAddress(address); } // Hex editor resized hexResize() { let tall = this.tall(false); let grew = this.lines.length < tall; // Process all visible lines for (let y = this.lines.length; y < tall; y++) { let line = { lblAddress: document.createElement("div"), lblBytes : [] }; // Address label line.lblAddress.className = "addr" + (y == 0 ? " first" : ""); this.hexEditor.append(line.lblAddress); // Byte labels for (let x = 0; x < 16; x++) { let lbl = line.lblBytes[x] = document.createElement("div"); lbl.className = "byte b" + x + (y == 0 ? " first" : ""); this.hexEditor.append(lbl); } this.lines.push(line); } // Remove lines that are no longer visible while (tall < this.lines.length) { let line = this.lines[tall]; line.lblAddress.remove(); for (let lbl of line.lblBytes) lbl.remove(); this.lines.splice(tall, 1); } // Configure scroll bar this.scrHex.hscroll.unitIncrement = this.sizer.element.getBoundingClientRect().height; // Update components if (grew) this.fetch(); else this.refresh(); } // Hex editor mouse wheel hexWheel(e) { // Error checking if (e.altKey || e.ctrlKey || e.shiftKey) return; // Always handle the event Toolkit.handle(e); // Determine how many full lines were scrolled let scr = Debugger.linesScrolled(e, this.sizer.element.getBoundingClientRect().height, this.tall(true), this.delta ); this.delta = scr.delta; scr.lines = Math.max(-3, Math.min(3, scr.lines)); // No lines were scrolled if (scr.lines == 0) return; // Scroll the view this.bus.viewAddress = this.bus.viewAddress + scr.lines * 16 >>> 0; this.fetch(); } // Window visibility onVisibility(e) { this.shown = this.shown || e.visible; if (!e.visible) this.debug.core.unsubscribe(this.subscription, false); else this.fetch(); } ///////////////////////////// Package Methods ///////////////////////////// // Prompt the user to navigate to a new editing address goto() { // Retrieve the value from the user let addr = prompt(this.app.localize("{debug.memory.goto}")); if (addr === null) return; addr = parseInt(addr.trim(), 16); if ( !Number.isInteger(addr) || addr < 0 || addr > 4294967295 ) return; // Commit an outstanding edit if (this.editDigit !== null && this.bus.editAddress != addr) this.commit(); // Navigate to the given address this.setEditAddress(addr, 1/3); } ///////////////////////////// Private Methods ///////////////////////////// // Write the edited value to the simulation state commit(refresh = true) { // Error checking if (this.editDigit === null) return; // The edited value is in the bus's data buffer if (this.data != null) { let offset = this.bus.editAddress - this.dataAddress >>> 0; if (offset < this.data.length) this.data[offset] = this.editDigit; } // Write one byte to the simulation state let data = new Uint8Array(1); data[0] = this.editDigit; this.editDigit = null; this.debug.core.write(this.debug.sim, this.bus.editAddress, data, { refresh: refresh }); } // Retrieve data from the simulation state async fetch() { // Select the parameters for the simulation fetch let params = { address: this.bus.viewAddress - 10 * 16, length : (this.tall(false) + 20) * 16 }; // A communication with the core thread is already underway if (this.pending) { this.pending = params; this.refresh(); return; } // Retrieve data from the simulation state this.pending = params; for (let data=null, promise=null; this.pending instanceof Object;) { // Wait for a transaction to complete if (promise != null) { this.pending = true; data = await promise; promise = null; } // Initiate a new transaction if (this.pending instanceof Object) { params = this.pending; let options = {}; if (this.isVisible()) options.subscription = this.subscription; promise = this.debug.core.read(this.debug.sim, params.address, params.length, options); } // Process the result of a transaction if (data != null) { this.refresh(data); data = null; } }; this.pending = false; } // Update hex editor refresh(msg = null) { // Receiving data from the simulation state if (msg != null) { this.data = msg.data; this.dataAddress = msg.address; } // Process all lines for (let y = 0; y < this.lines.length; y++) { let address = this.bus.viewAddress + y * 16 >>> 0; let line = this.lines[y]; // Address label line.lblAddress.innerText = this.debug.hex(address, 8, false); // Process all bytes for (let x = 0; x < 16; x++) { let label = line.lblBytes[x]; let text = "--"; // Currently editing this byte if (address+x==this.bus.editAddress && this.editDigit!==null) { text = this.debug.hex(this.editDigit, 1, false); } // Bus data exists else if (this.data != null) { let offset = address - this.dataAddress + x >>> 0; // The byte is contained in the bus data buffer if (offset >= 0 && offset < this.data.length) text = this.debug.hex(this.data[offset], 2, false); } label.innerText = text; label.classList[address + x == this.bus.editAddress ? "add" : "remove"]("edit"); } } } // Specify the address of the hex editor's selection setEditAddress(address, auto = false) { let col = this.lines[0].lblBytes[address&15].getBoundingClientRect(); let port = this.scrHex.viewport.element.getBoundingClientRect(); let row = (address & ~15) >>> 0; let scr = this.scrHex.scrollLeft; let tall = this.tall(true, 0); // Ensure the data row is fully visible if (row - this.bus.viewAddress >>> 0 >= tall * 16) { if (!auto) { this.bus.viewAddress = this.bus.viewAddress - row >>> 0 <= row - (this.bus.viewAddress + tall * 16) >>> 0 ? row : row - (tall - 1) * 16; } else this.bus.viewAddress = row - Math.floor(tall * auto) * 16; this.fetch(); } // Ensure the column is fully visible this.scrHex.scrollLeft = Math.min( Math.max( scr, scr + col.right - port.right ), scr - port.x + col.x ) ; // Refresh the display; this.bus.editAddress = address; this.refresh(); } // Measure the number of lines visible in the view tall(fully = null, plus = 1) { return Math.max(1, Math[fully===null ? "abs" : fully?"floor":"ceil"]( this.scrHex.viewport.element.getBoundingClientRect().height / this.sizer .element.getBoundingClientRect().height )) + plus; } } export { register };