pvbemu/src/desktop/app/CharactersWindow.java

613 lines
22 KiB
Java

package app;
// Java imports
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.util.*;
import javax.swing.*;
// Project imports
import util.*;
// VIP characters window
class CharactersWindow extends ChildWindow {
// Instance fields
private int color; // Selected color index
private Point dragging; // Most recent pattern mouse position
private int index; // Current character index
private boolean grid; // Draw a grid around characters
private int palette; // Palette index
private int[] pix; // Image buffer
private int scale; // Display scale
private int wide; // Number of characters per row
private BufferedImage image; // Character graphics
// UI components
private JCheckBox chkGrid; // Grid check box
private UPanel client; // Client area
private UComboBox cmbPalette; // Palette drop-down
private UPanel panCharacters; // Characters panel
private UPanel panPalette; // Palette panel
private UPanel panPattern; // Pattern panel
private JScrollPane scrCharacters; // Characters container
private JScrollPane scrControls; // Controls panel
private JScrollBar scrWidth; // Width template scroll bar
private JSlider sldScale; // Scale slider
private JSpinner spnIndex; // Index spinner
private JSpinner spnWide; // Wide spinner
private JTextField txtAddress; // Address text box
private JTextField txtMirror; // Mirror text box
///////////////////////////////////////////////////////////////////////////
// Constructors //
///////////////////////////////////////////////////////////////////////////
// Default constructor
CharactersWindow(MainWindow parent) {
super(parent, "characters.title");
// Configure instance fields
color = 0;
grid = true;
image = new BufferedImage(256, 512, BufferedImage.TYPE_INT_RGB);
pix = new int[256 * 512];
scale = 3;
// Template scroll bar to check control width in the current LAF
scrWidth = new JScrollBar(JScrollBar.VERTICAL);
// Configure client area
client = new UPanel(new BorderLayout());
client.setBackground(SystemColor.control);
client.setFocusable(true);
client.setPreferredSize(new Dimension(480, 360));
client.addComponentListener(Util.onResize(e->onResize()));
// Configure controls panel
var ctrls = new UPanel(new GridBagLayout());
ctrls.setBackground(SystemColor.control);
ctrls.addMouseListener(
Util.onMouse(e->client.requestFocus(), null));
scrControls = new JScrollPane(ctrls,
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scrControls.setBorder(null);
scrControls.getVerticalScrollBar().setUnitIncrement(20);
client.add(scrControls, BorderLayout.WEST);
// Character controls
label(ctrls, "characters.index", true);
spnIndex = spinner(ctrls, 0, 2047, 0, true);
spnIndex.addChangeListener(e->
setIndex((Integer) spnIndex.getValue()));
label(ctrls, "characters.address", false);
txtAddress = textBox(ctrls);
txtAddress.addFocusListener(Util.onFocus(null,
e->onAddress(txtAddress)));
label(ctrls, "characters.mirror", false);
txtMirror = textBox(ctrls);
txtMirror.addFocusListener(Util.onFocus(null,
e->onAddress(txtMirror)));
// Pattern panel
panPattern = new UPanel();
panPattern.setOpaque(false);
panPattern.setPreferredSize(new Dimension(96, 96));
panPattern.addMouseListener(
Util.onMouse(e->onMousePattern(e), e->onMousePattern(e)));
panPattern.addMouseMotionListener(
Util.onMouseMove(null, e->onMousePattern(e)));
panPattern.addPaintListener((g,w,h)->onPaintPattern(g, w, h));
var gbc = new GridBagConstraints();
gbc.anchor = GridBagConstraints.CENTER;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.insets = new Insets(4, 2, 2, 2);
ctrls.add(panPattern, gbc);
// Palette panel
panPalette = new UPanel();
panPalette.setOpaque(false);
panPalette.setPreferredSize(new Dimension(0, 20 + 6));
panPalette.addMouseListener(
Util.onMouse(e->onMouseDownPalette(e), null));
panPalette.addPaintListener((g,w,h)->onPaintPalette(g, w, h));
gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.insets = new Insets(0, 2, 4, 2);
ctrls.add(panPalette, gbc);
// Fill the extra space above the view controls
var spacer = new UPanel();
spacer.setOpaque(false);
gbc = new GridBagConstraints();
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.weighty = 1;
ctrls.add(spacer, gbc);
// View controls
label(ctrls, "characters.grid", false);
chkGrid = checkBox(ctrls);
chkGrid.setSelected(grid);
chkGrid.addActionListener(e->{
grid = chkGrid.isSelected(); onView(); });
label(ctrls, "characters.wide", false);
spnWide = spinner(ctrls, 0, 2048, 0, false);
spnWide.addChangeListener(e->{
wide = (Integer) spnWide.getValue(); onView(); });
label(ctrls, "characters.palette", false);
cmbPalette = select(ctrls, new String[] { "palette.generic",
"palette.gplt0", "palette.gplt1", "palette.gplt2", "palette.gplt3",
"palette.jplt0", "palette.jplt1", "palette.jplt2", "palette.jplt3"
});
cmbPalette.addActionListener(e->{
palette = cmbPalette.getSelectedIndex(); repaint(); });
label(ctrls, "characters.scale", false);
sldScale = slider(ctrls, 1, 10, scale);
sldScale.addChangeListener(e->{
scale = sldScale.getValue(); onView(); });
// Terminate the list of controls
spacer = new UPanel();
spacer.setOpaque(false);
spacer.setPreferredSize(new Dimension(0, 0));
gbc = new GridBagConstraints();
gbc.gridheight = GridBagConstraints.REMAINDER;
gbc.gridwidth = GridBagConstraints.REMAINDER;
ctrls.add(spacer, gbc);
// Configure characters panel
panCharacters = new UPanel();
panCharacters.setBackground(SystemColor.control);
panCharacters.addMouseListener(
Util.onMouse(e->onMouseCharacters(e), e->onMouseCharacters(e)));
panCharacters.addMouseMotionListener(
Util.onMouseMove(null, e->onMouseCharacters(e)));
panCharacters.addPaintListener((g,w,h)->onPaintCharacters(g, w, h));
scrCharacters = new JScrollPane(panCharacters);
client.add(scrCharacters, BorderLayout.CENTER);
// Configure component
setContentPane(client);
setIndex(0);
onView();
pack();
}
///////////////////////////////////////////////////////////////////////////
// Package Methods //
///////////////////////////////////////////////////////////////////////////
// Update the display
void refresh() {
// Update characters graphic
var pal = parent.palettes[palette][MainWindow.RED];
for (int index = 0; index < 2048; index++)
parent.drawCharacter(index, false, false, pal, pix,
index >> 5 << 11 | (index & 31) << 3, 256);
image.setRGB(0, 0, 256, 512, pix, 0, 256);
// Update display;
panCharacters.repaint();
panPattern .repaint();
}
///////////////////////////////////////////////////////////////////////////
// Event Handlers //
///////////////////////////////////////////////////////////////////////////
// Address and Mirror text box commit
private void onAddress(JTextField src) {
int address;
// Parse the given address
try { address = (int) Long.parseLong(src.getText(), 16); }
catch (Exception e) {
setIndex(index);
return;
}
// Restrict address range
if ((address >> 24 & 7) != 0) {
setIndex(index);
return;
}
address &= 0x0007FFFF;
// Character memory
if ((address & 0x00066000) == 0x00006000)
index = address >> 15 << 9 | address >> 4 & 511;
// Mirror of character memory
else if (address >= 0x00078000)
index = address - 0x00078000 >> 4;
// Common processing
setIndex(index);
}
// Characters mouse
private void onMouseCharacters(MouseEvent e) {
int id = e.getID();
// Mouse release
if (id == MouseEvent.MOUSE_RELEASED) {
if (e.getButton() == MouseEvent.BUTTON1)
dragging = null;
return;
}
// Mouse press
if (id == MouseEvent.MOUSE_PRESSED) {
client.requestFocus();
if (e.getButton() != MouseEvent.BUTTON1)
return;
dragging = e.getPoint();
}
// Not dragging
if (dragging == null)
return;
// Working variables
int grid = this.grid ? 1 : 0;
int size = 8 * scale + grid;
int wide = this.wide != 0 ? this.wide :
Math.max(1, (panCharacters.getWidth() + grid) / size);
int col = e.getX() / size;
int row = e.getY() / size;
// The pixel is not within a character
if (col >= wide || row >= (2047 + wide) / wide)
return;
// Calculate the index of the selected character
int index = row * wide + col;
if (index < 2048)
setIndex(index);
}
// Palette mouse button press
private void onMouseDownPalette(MouseEvent e) {
// Common processing
client.requestFocus();
// Only consider left clicks
if (e.getButton() != MouseEvent.BUTTON1)
return;
// Working variables
int height = panPalette.getHeight();
int width = panPalette.getWidth();
int size = Math.max(1, Math.min((width - 18) / 4, height - 6));
int left = (width - size * 4 - 18) / 2;
int top = (height - size - 6) / 2;
int x = e.getX() - left - 3;
int y = e.getY() - top - 3;
// The click was not on top of a color
if (
x < 0 || x >= (size + 4) * 4 ||
x % (size + 4) >= size ||
y < 0 || y >= size
) return;
// Select the clicked color
color = x / (size + 4);
panPalette.repaint();
}
// Pattern mouse
private void onMousePattern(MouseEvent e) {
int id = e.getID();
int scale = Math.max(1, Math.min(
panPattern.getWidth () >> 3,
panPattern.getHeight() >> 3
));
// Mouse release
if (id == MouseEvent.MOUSE_RELEASED) {
if (e.getButton() == MouseEvent.BUTTON1)
dragging = null;
return;
}
// Mouse press
if (id == MouseEvent.MOUSE_PRESSED) {
client.requestFocus();
if (e.getButton() != MouseEvent.BUTTON1)
return;
dragging = e.getPoint();
dragging.x = dragging.x / scale - (dragging.x < 0 ? 1 : 0);
dragging.y = dragging.y / scale - (dragging.y < 0 ? 1 : 0);
}
// Not dragging
if (dragging == null)
return;
// Retrieve the current point
var pos = e.getPoint();
pos.x = pos.x / scale - (pos.x < 0 ? 1 : 0);
pos.y = pos.y / scale - (pos.y < 0 ? 1 : 0);
// Draw a line segment (adapted from Wikipedia)
int dx = Math.abs(pos.x - dragging.x);
int sx = dragging.x < pos.x ? 1 : -1;
int dy = -Math.abs(pos.y - dragging.y);
int sy = dragging.y < pos.y ? 1 : -1;
int err = dx + dy;
int addr = index >> 9 << 15 | 0x00006000 | (index & 511) << 4;
for (;;) {
// Draw the current pixel
if ((dragging.x&7) == dragging.x && (dragging.y&7) == dragging.y) {
int b = addr | dragging.y << 1 | dragging.x >> 2;
int bit = (dragging.x & 3) << 1;
parent.vram[b] = (byte)
(parent.vram[b] & ~(3 << bit) | color << bit);
}
// The last pixel has been drawn
if (dragging.x == pos.x && dragging.y == pos.y)
break;
// Advance to the next pixel
int e2 = err << 1;
if (e2 >= dy)
{ err += dy; dragging.x += sx; }
if (e2 <= dx)
{ err += dx; dragging.y += sy; }
}
// Update VRAM state
parent.vue.writeBytes(addr, parent.vram, addr, 16);
parent.refreshDebug(false);
}
// Characters paint
private void onPaintCharacters(Graphics2D g, int width, int height) {
int grid = this.grid ? 1 : 0;
int size = scale * 8 + grid;
int wide = this.wide != 0 ? this.wide :
Math.max(1, (width + grid) / size);
int tall = (2047 + wide) / wide;
// Draw all characters
for (int Y = 0, y = 0, z = 0; y < tall; y++, Y += size)
for (int X = 0, x = 0; x < wide; x++, z++, X += size) {
if (z == 2048)
break;
int ix = (z & 31) << 3;
int iy = z >> 5 << 3;
g.drawImage(image,
X, Y, X + size - grid, Y + size - grid,
ix, iy, ix + 8 , iy + 8 ,
null);
}
// Highlight the selected character
int X = index % wide * size;
int Y = index / wide * size;
int light = SystemColor.textHighlight.getRGB() & 0x00FFFFFF;
g.setColor(new Color(0xD0000000 | light, true));
g.drawRect(X , Y , 8 * scale - 1, 8 * scale - 1);
g.setColor(new Color(0x90000000 | light, true));
g.drawRect(X + 1, Y + 1, 8 * scale - 3, 8 * scale - 3);
g.setColor(new Color(0x50000000 | light, true));
g.fillRect(X + 2, Y + 2, 8 * scale - 4, 8 * scale - 4);
}
// Palette paint
private void onPaintPalette(Graphics2D g, int width, int height) {
int size = Math.max(1, Math.min((width - 18) / 4, height - 6));
int left = (width - size * 4 - 18) / 2;
var pal = parent.palettes[palette][MainWindow.RED];
int top = (height - size - 6) / 2;
// Draw the color picker
for (int x = 0; x < 4; x++, left += size + 4) {
// The current color is selected
if (x == color) {
g.setColor(SystemColor.textHighlight);
g.fillRect(left , top , size + 6, size + 6);
g.setColor(SystemColor.control);
g.fillRect(left + 2, top + 2, size + 2, size + 2);
}
// Draw the color area
g.setColor(new Color(pal[x]));
g.fillRect(left + 3, top + 3, size , size );
}
}
// Pattern paint
private void onPaintPattern(Graphics2D g, int width, int height) {
int scale = Math.max(1, Math.min(width >> 3, height >> 3));
int x = (width - (scale << 3) + 1) >> 1;
int y = (height - (scale << 3) + 1) >> 1;
int ix = (index & 31) << 3;
int iy = index >> 5 << 3;
g.drawImage(image,
x, y, x + scale * 8, y + scale * 8,
ix, iy, ix + 8 , iy + 8 ,
null);
}
// Window resize
private void onResize() {
var viewport = scrControls.getViewport();
int inner = viewport.getView().getPreferredSize().width;
int outer = viewport.getExtentSize().width;
// Size the controls container to match the inner component
if (inner != outer) {
scrControls.setPreferredSize(new Dimension(
scrControls.getPreferredSize().width + inner - outer, 0));
scrControls.revalidate();
scrControls.repaint();
}
// The number of characters per row is dynamic
if (wide == 0) {
var bar = scrWidth.getPreferredSize().width;
var border = scrCharacters.getBorder();
int grid = this.grid ? 1 : 0;
var insets = border == null ? new Insets(0, 0, 0, 0) :
border.getBorderInsets(scrCharacters);
var size = new Dimension(
scrCharacters.getWidth () - insets.left - insets.right,
scrCharacters.getHeight() - insets.top - insets.bottom
);
// Calculate the dimensions without the vertical scroll bar
int wide = Math.max(1, (size.width + grid) / (8 * scale + grid));
int height = (2047 + wide) / wide * (8 * scale + grid) - grid;
// Calculate the dimensions with the vertical scroll bar
if (height > size.height) {
wide = Math.max(1, (size.width-bar+grid) / (8 * scale + grid));
height = (2047 + wide) / wide * (8 * scale + grid) - grid;
}
// Configure the component
panCharacters.setPreferredSize(
new Dimension(wide * (8 * scale + grid) - grid, height));
panCharacters.revalidate();
panCharacters.repaint();
}
}
// View control changed
private void onView() {
// Number of characters per row is dynamic: defer to resize processing
if (wide == 0) {
onResize();
return;
}
// Calculate the space occupied by the character images
int grid = this.grid ? 1 : 0;
panCharacters.setPreferredSize(new Dimension(
wide * (scale * 8 + grid) - grid,
(2047 + wide) / wide * (scale * 8 + grid) - grid
));
scrCharacters.getVerticalScrollBar().setUnitIncrement(8*scale+grid);
panCharacters.revalidate();
panCharacters.repaint();
}
///////////////////////////////////////////////////////////////////////////
// Private Methods //
///////////////////////////////////////////////////////////////////////////
// Add a check box to the controls panel
private JCheckBox checkBox(UPanel panel) {
var chk = new JCheckBox();
chk.setBorder(null);
chk.setFocusable(false);
var gbc = new GridBagConstraints();
gbc.anchor = GridBagConstraints.WEST;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.insets = new Insets(0, 0, 2, 2);
panel.add(chk, gbc);
return chk;
}
// Add a label to the controls panel
private void label(UPanel panel, String key, boolean top) {
var lbl = new JLabel();
parent.app.localizer.add(lbl, key);
var gbc = new GridBagConstraints();
gbc.anchor = GridBagConstraints.WEST;
gbc.insets = new Insets(top ? 2 : 0, 2, 2, 2);
panel.add(lbl, gbc);
}
// Add a combo box to the controls panel
private UComboBox select(UPanel panel, String[] options) {
var cmb = new UComboBox();
parent.app.localizer.add(cmb, options);
cmb.setSelectedIndex(0);
var gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.BOTH;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.insets = new Insets(0, 0, 2, 2);
panel.add(cmb, gbc);
return cmb;
}
// Specify the current character index
private void setIndex(int index) {
this.index = index;
spnIndex.setValue(index);
txtAddress.setText(String.format("%08X",
index >> 9 << 15 | 0x00006000 | (index & 511) << 4));
txtMirror .setText(String.format("%08X",
index << 4 | 0x00078000));
repaint();
}
// Add a slider to the controls panel
private JSlider slider(UPanel panel, int min, int max, int value) {
var sld = new JSlider(min, max, value);
sld.setFocusable(false);
sld.setPreferredSize(new Dimension(0, sld.getPreferredSize().height));
var gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.BOTH;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.insets = new Insets(0, 0, 1, 2);
panel.add(sld, gbc);
return sld;
}
// Add a spinner to the controls panel
private JSpinner spinner(UPanel panel, int min, int max, int value,
boolean top) {
var spn = new JSpinner(new SpinnerNumberModel(value, min, max, 1));
var txt = new JSpinner.NumberEditor(spn, "#");
txt.getTextField().addActionListener(e->client.requestFocus());
spn.setEditor(txt);
var gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.BOTH;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.insets = new Insets(top ? 2 : 0, 0, 2, 2);
gbc.weightx = 1;
panel.add(spn, gbc);
return spn;
}
// Add a text box to the controls panel
private JTextField textBox(UPanel panel) {
var txt = new JTextField();
txt.setFont(parent.app.fntMono);
txt.addActionListener(e->client.requestFocus());
var size = txt.getPreferredSize();
txt.setPreferredSize(new Dimension(
parent.app.hexDigitWidth * 8 + 2 + size.width, size.height));
var gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.BOTH;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.insets = new Insets(0, 0, 2, 2);
panel.add(txt, gbc);
return txt;
}
}