pvbemu/src/desktop/app/MemoryWindow.java

270 lines
8.4 KiB
Java

package app;
// Java imports
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
// Project imports
import util.*;
// Memory viewer and hex editor window
class MemoryWindow extends ChildWindow {
// Private fields
private int address; // Address of top row
private Font font; // Display font
private int fontHeight; // Font line height
private int fontWidth; // Font maximum character width
// UI components
private JPanel client; // Client area
private ArrayList<Row> rows; // Rows of text
///////////////////////////////////////////////////////////////////////////
// Classes //
///////////////////////////////////////////////////////////////////////////
// One row of output
private class Row {
JLabel address; // Address (row header)
JLabel[] bytes; // Hexadecimal bytes
}
///////////////////////////////////////////////////////////////////////////
// Constructors //
///////////////////////////////////////////////////////////////////////////
// Default constructor
MemoryWindow(MainWindow parent) {
super(parent, "memory.title");
// Configure instance fields
address = 0x00000000;
font = new Font(Util.fontFamily(new String[]
{ "Consolas", Font.MONOSPACED } ), Font.PLAIN, 14);
rows = new ArrayList<Row>();
// Configure client area
client = new JPanel(null);
client.addComponentListener(Util.onResize(e->onResize()));
client.addKeyListener(Util.onKey(e->onKeyDown(e), null));
client.addMouseWheelListener(e->onWheel(e));
client.setBackground(SystemColor.window);
client.setFocusable(true);
client.setPreferredSize(new Dimension(480, 360));
// Configure component
var content = new JPanel(new BorderLayout());
content.setBorder(new JScrollPane().getBorder());
content.add(client, BorderLayout.CENTER);
setContentPane(content);
setFont2(font);
pack();
}
///////////////////////////////////////////////////////////////////////////
// Package Methods //
///////////////////////////////////////////////////////////////////////////
// Update the display
void refresh() {
// The element is not ready
if (client == null)
return;
// Configure working variables
int height = client.getHeight();
int count = (height + fontHeight - 1) / fontHeight;
var data = new byte[count * 16];
// Retrieve all visible bytes from the emulation context
parent.vue.read(address, data, 0, data.length);
// Update visible rows
for (int x = 0; x < count; x++) {
Row row;
if (x < rows.size())
row = rows.get(x); // Retrieve row from collection
else row = createRow(); // Produce a new row
update(row, x * fontHeight, address + x * 16, data, x * 16);
setVisible(row, true);
}
// Hide any rows that are not visible
for (int x = count; x < rows.size(); x++)
setVisible(rows.get(x), false);
// Finalize layout
client.revalidate();
client.repaint();
}
// Specify a new font
void setFont2(Font font) {
this.font = font;
// Configure the maximum font dimensions
var fontMax = new JLabel("!");
fontMax.setFont(font);
fontHeight = Math.max(1, fontMax.getPreferredSize().height);
fontWidth = -1;
for (int x = 0; x < 16; x++) {
fontMax.setText(Integer.toString(x, 16).toUpperCase());
fontWidth = Math.max(fontWidth, fontMax.getPreferredSize().width);
}
// Configure rows
for (var row : rows) {
row.address.setFont(font);
for (var label : row.bytes)
label.setFont(font);
}
onResize();
}
///////////////////////////////////////////////////////////////////////////
// Event Handlers //
///////////////////////////////////////////////////////////////////////////
// Key down
private void onKeyDown(KeyEvent e) {
int code = e.getKeyCode();
int mods = e.getModifiersEx();
boolean alt = (mods & InputEvent.ALT_DOWN_MASK ) != 0;
boolean ctrl = (mods & InputEvent.CTRL_DOWN_MASK) != 0;
int tall = Math.max(1, tall(false));
// No Alt combinations
if (alt) return;
// Goto
if (ctrl && code == KeyEvent.VK_G) {
String addr = JOptionPane.showInputDialog(
this, "Goto:", "Goto", JOptionPane.PLAIN_MESSAGE);
if (addr != null && addr.trim().length() != 0) {
try { setAddress((int) Long.parseLong(addr, 16)); }
catch (Exception x) { }
}
return;
}
// Seek
switch (code) {
case KeyEvent.VK_UP : setAddress(address - 16); break;
case KeyEvent.VK_DOWN : setAddress(address + 16); break;
case KeyEvent.VK_PAGE_UP : setAddress(address - tall * 16); break;
case KeyEvent.VK_PAGE_DOWN: setAddress(address + tall * 16); break;
}
}
// Client resize
private void onResize() {
refresh();
}
// Mouse wheel
private void onWheel(MouseWheelEvent e) {
int amount = e.getUnitsToScroll();
int mods = e.getModifiersEx();
boolean alt = (mods & InputEvent.ALT_DOWN_MASK ) != 0;
boolean ctrl = (mods & InputEvent.CTRL_DOWN_MASK) != 0;
// No Alt or Ctrl combinations
if (amount == 0 || alt || ctrl)
return;
// Seek
setAddress(address + 16 * amount);
}
///////////////////////////////////////////////////////////////////////////
// Private Methods //
///////////////////////////////////////////////////////////////////////////
// Add a new row of output
private Row createRow() {
var row = new Row();
// Address label
row.address = new JLabel();
row.address.setFont(font);
row.address.setForeground(SystemColor.windowText);
row.address.setVisible(false);
client.add(row.address);
// Byte labels
row.bytes = new JLabel[16];
for (int x = 0; x < row.bytes.length; x++) {
var label = row.bytes[x] = new JLabel();
label.setFont(font);
label.setForeground(SystemColor.windowText);
label.setHorizontalAlignment(SwingConstants.CENTER);
label.setVisible(false);
client.add(label);
}
// Add the row to the collection
rows.add(row);
return row;
}
// Measure the minimum column widths for a row
private void measure(Row row, int[] widths) {
widths[0] = Math.max(widths[0], row.address.getPreferredSize().width);
for (int x = 0; x < 16; x++)
widths[1] = Math.max(widths[1],
row.bytes[x].getPreferredSize().width);
}
// Specify the address of the top row of output
private void setAddress(int address) {
this.address = address & 0xFFFFFFF0;
refresh();
}
// Show or hide a row
private void setVisible(Row row, boolean visible) {
row.address.setVisible(visible);
for (var label : row.bytes)
label.setVisible(visible);
}
// Determine how many rows of output are visible
private int tall(boolean partial) {
return (client.getHeight() + (partial?fontHeight-1:0)) / fontHeight;
}
// Update the text of a row
private void update(Row row, int y, int address, byte[] data, int offset) {
// Update address
row.address.setBounds(0, y, 8 * fontWidth, fontHeight);
row.address.setText(String.format("%08X", address));
// Update bytes
for (int z = 0, x = 10 * fontWidth; z < 16; z++) {
var label = row.bytes[z];
label.setBounds(x, y, 2 * fontWidth, fontHeight);
label.setText(String.format("%02X", data[offset++] & 0xFF));
x += fontWidth * (z == 7 ? 4 : 3);
}
}
}