pvbemu/src/desktop/app/BGMapsWindow.java

513 lines
18 KiB
Java

package app;
// Java imports
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.util.*;
import javax.swing.*;
import javax.swing.border.*;
// Project imports
import util.*;
import vue.*;
// VIP backgrounds window
class BGMapsWindow extends ChildWindow {
// Instance fields
private int cellIndex; // Current cell index
private int character; // Cell character index
private Point dragging; // Most recent pattern mouse position
private boolean generic; // Use the generic palette
private boolean grid; // Draw a grid around characters
private boolean hFlip; // Cell is flipped horizontally
private int mapIndex; // Current BG map index
private int palette; // Palette index
private int[] pix; // Image buffer
private int scale; // Display scale
private boolean vFlip; // Cell is flipped vertically
private BufferedImage image; // BG map graphic
// UI components
private JCheckBox chkGeneric; // Generic colors check box
private JCheckBox chkGrid; // Grid check box
private JCheckBox chkHFlip; // H-flip check box
private JCheckBox chkVFlip; // V-flip check box
private UComboBox cmbPalette; // Palette drop-down
private UPanel client; // Client area
private UPanel panCell; // Cell panel
private UPanel panMap; // BG map panel
private JScrollPane scrControls; // Controls panel
private JScrollPane scrMap; // BG map container
private JSlider sldScale; // Scale slider
private JSpinner spnCellIndex; // Cell index spinner
private JSpinner spnCharacter; // Character spinner
private JSpinner spnMapIndex; // BG map index spinner
private JTextField txtCellAddress; // Cell address text box
private JTextField txtMapAddress; // BG map address text box
///////////////////////////////////////////////////////////////////////////
// Constructors //
///////////////////////////////////////////////////////////////////////////
// Default constructor
BGMapsWindow(MainWindow parent) {
super(parent, "bg_maps.title");
// Configure instance fields
generic = false;
image = new BufferedImage(512, 512, BufferedImage.TYPE_INT_RGB);
pix = new int[512 * 512];
scale = 2;
// 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);
// BG Map controls
label(ctrls, "bg_maps.map", true);
spnMapIndex = spinner(ctrls, 0, 15, 0, true);
spnMapIndex.addChangeListener(e->
setMap((Integer) spnMapIndex.getValue()));
label(ctrls, "bg_maps.address", false);
txtMapAddress = textBox(ctrls);
txtMapAddress.addFocusListener(Util.onFocus(null,
e->onAddress(txtMapAddress)));
// Cell controls container
var cell = new UPanel(new GridBagLayout());
cell.setBorder(new TitledBorder(""));
parent.app.localizer.add(cell, "bg_maps.cell");
var gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.insets = new Insets(0, 2, 2, 2);
ctrls.add(cell, gbc);
// Cell controls
label(cell, "bg_maps.index", true);
spnCellIndex = spinner(cell, 0, 4095, 0, true);
spnCellIndex.addChangeListener(e->
setCell((Integer) spnCellIndex.getValue()));
label(cell, "bg_maps.address", false);
txtCellAddress = textBox(cell);
txtCellAddress.addFocusListener(Util.onFocus(null,
e->onAddress(txtCellAddress)));
label(cell, "bg_maps.character", false);
spnCharacter = spinner(cell, 0, 2047, 0, false);
spnCharacter.addChangeListener(e->onEdit());
label(cell, "bg_maps.palette", false);
cmbPalette = select(cell, new String[] {
"palette.gplt0", "palette.gplt1", "palette.gplt2", "palette.gplt3"
});
cmbPalette.addActionListener(e->onEdit());
label(cell, "bg_maps.hflip", false);
chkHFlip = checkBox(cell);
chkHFlip.addActionListener(e->onEdit());
label(cell, "bg_maps.vflip", false);
chkVFlip = checkBox(cell);
chkVFlip.addActionListener(e->onEdit());
// Cell panel
panCell = new UPanel();
panCell.setOpaque(false);
panCell.setPreferredSize(new Dimension(96, 96));
panCell.addPaintListener((g,w,h)->onPaintCell(g, w, h));
gbc = new GridBagConstraints();
gbc.anchor = GridBagConstraints.CENTER;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.insets = new Insets(2, 2, 2, 2);
cell.add(panCell, gbc);
terminator(cell);
// 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, "bg_maps.grid", false);
chkGrid = checkBox(ctrls);
chkGrid.addActionListener(e->{
grid = chkGrid.isSelected(); onView(); });
label(ctrls, "bg_maps.generic", false);
chkGeneric = checkBox(ctrls);
chkGeneric.setSelected(generic);
chkGeneric.addActionListener(e->{
generic = chkGeneric.isSelected(); refresh(); });
label(ctrls, "bg_maps.scale", false);
sldScale = slider(ctrls, 1, 10, scale);
sldScale.addChangeListener(e->{
scale = sldScale.getValue(); onView(); });
terminator(ctrls);
// Configure BG map panel
panMap = new UPanel();
panMap.setBackground(SystemColor.control);
panMap.addMouseListener(
Util.onMouse(e->onMouse(e), e->onMouse(e)));
panMap.addMouseMotionListener(
Util.onMouseMove(null, e->onMouse(e)));
panMap.addPaintListener((g,w,h)->onPaintMap(g, w, h));
scrMap = new JScrollPane(panMap);
client.add(scrMap, BorderLayout.CENTER);
// Configure component
setContentPane(client);
setCell(0);
onView();
pack();
}
///////////////////////////////////////////////////////////////////////////
// Package Methods //
///////////////////////////////////////////////////////////////////////////
// Update the display
void refresh() {
// Update BG map graphic
int src = 0x00020000 | mapIndex << 13;
for (int index = 0; index < 4096; index++, src += 2) {
int bits = parent.vram[src] & 0xFF | parent.vram[src + 1] << 8;
var pal = parent.palettes[generic ? MainWindow.GENERIC :
MainWindow.GPLT0 + (bits >> 14 & 3)][MainWindow.RED];
parent.drawCharacter(
bits & 0x07FF, (bits & 0x2000) != 0, (bits & 0x1000) != 0, pal,
pix, index >> 6 << 12 | (index & 63) << 3, 512
);
}
image.setRGB(0, 0, 512, 512, pix, 0, 512);
// Update display;
panCell.repaint();
panMap .repaint();
}
///////////////////////////////////////////////////////////////////////////
// Event Handlers //
///////////////////////////////////////////////////////////////////////////
// Address text boxes commit
private void onAddress(JTextField src) {
int address;
// Parse the given address
try { address = (int) Long.parseLong(src.getText(), 16); }
catch (Exception e) {
setMap(mapIndex);
return;
}
// Restrict address range
if ((address >> 24 & 7) != 0) {
setMap(mapIndex);
return;
}
address &= 0x0007FFFF;
// Check if the address is in BG map memory
if (address < 0x00020000 || address >= 0x00040000) {
setMap(mapIndex);
return;
}
// Update the map and character indexes
address -= 0x00020000;
mapIndex = address >> 13;
setCell((address & 0x00001FFE) >> 1);
}
// A cell value has changed
private void onEdit() {
// Process cell properties
character = (Integer) spnCharacter.getValue();
hFlip = chkHFlip.isSelected();
palette = cmbPalette.getSelectedIndex();
vFlip = chkVFlip.isSelected();
// Update VRAM state
parent.vue.write(
0x00020000 | mapIndex << 13 | cellIndex << 1, Vue.U16,
palette << 14 | (hFlip ? 0x2000 : 0) |
(vFlip ? 0x1000 : 0) | character
);
parent.refreshDebug(false);
}
// BG map panel mouse
private void onMouse(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 col = e.getX() / size;
int row = e.getY() / size;
// The pixel is not within a cell
if (col >= 64 || row >= 64)
return;
// Calculate the index of the selected cell
setCell(row << 6 | col);
}
// Cell panel paint
private void onPaintCell(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 = (cellIndex & 63) << 3;
int iy = cellIndex >> 6 << 3;
g.drawImage(image,
x, y, x + scale * 8, y + scale * 8,
ix, iy, ix + 8 , iy + 8 ,
null);
}
// BG map panel paint
private void onPaintMap(Graphics2D g, int width, int height) {
// Drawing with a grid
if (grid) {
int scale = 8 * this.scale;
for (int y = 0, z = 0; y < 64; y++)
for (int x = 0; x < 64; x++, z++) {
int ix = (z & 63) << 3;
int iy = z >> 6 << 3;
int ox = x * (scale + 1);
int oy = y * (scale + 1);
g.drawImage(image,
ox, oy, ox + scale, oy + scale,
ix, iy, ix + 8 , iy + 8 ,
null);
}
}
// Not drawing with a grid
else g.drawImage(image, 0, 0, 512 * scale, 512 * scale, null);
// Highlight the selected cell
int size = 8 * scale + (grid ? 1 : 0);
int X = (cellIndex & 63) * size;
int Y = (cellIndex >> 6) * 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);
}
// 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();
}
}
// View control changed
private void onView() {
int grid = this.grid ? 1 : 0;
int size = 8 * scale + grid;
int pref = size * 64 - grid;
scrMap.getVerticalScrollBar().setUnitIncrement(8 * scale + grid);
panMap.setPreferredSize(new Dimension(pref, pref));
panMap.revalidate();
panMap.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);
}
// Update controls
private void refreshLite() {
int mapAddr = mapIndex << 13 | 0x00020000;
int cellAddr = mapAddr | cellIndex << 1;
int bits = parent.vram[cellAddr] & 0xFF | parent.vram[cellAddr+1] << 8;
character = bits & 0x07FF;
hFlip = (bits & 0x2000) != 0;
palette = bits >> 14 & 3;
vFlip = (bits & 0x1000) != 0;
chkHFlip .setSelected(hFlip);
chkVFlip .setSelected(vFlip);
cmbPalette .setSelectedIndex(palette);
spnCellIndex .setValue(cellIndex);
spnCharacter .setValue(character);
spnMapIndex .setValue(mapIndex);
txtMapAddress .setText(String.format("%08X", mapAddr ));
txtCellAddress.setText(String.format("%08X", cellAddr));
panCell.repaint();
panMap .repaint();
}
// 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 cell index
private void setCell(int index) {
cellIndex = index;
refreshLite();
}
// Specify the current BG map index
private void setMap(int index) {
mapIndex = index;
refreshLite();
}
// 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;
}
// Terminate a controls container
private void terminator(UPanel panel) {
var spacer = new UPanel();
spacer.setOpaque(false);
spacer.setPreferredSize(new Dimension(0, 0));
var gbc = new GridBagConstraints();
gbc.gridheight = GridBagConstraints.REMAINDER;
gbc.gridwidth = GridBagConstraints.REMAINDER;
panel.add(spacer, gbc);
}
// 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;
}
}