Introducing BG Maps window, performance adjustments for Characters window
This commit is contained in:
parent
8192fb6960
commit
c6d5332828
|
@ -14,7 +14,7 @@ app {
|
|||
cpu CPU
|
||||
memory Memory
|
||||
# VIP section
|
||||
backgrounds Backgrounds
|
||||
bg_maps BG Maps
|
||||
characters Characters
|
||||
frame_buffers Frame buffers
|
||||
objects Objects
|
||||
|
@ -39,6 +39,22 @@ app {
|
|||
|
||||
}
|
||||
|
||||
# BG Maps window
|
||||
bg_maps {
|
||||
address Address
|
||||
cell Cell
|
||||
character Character
|
||||
generic Generic
|
||||
grid Grid
|
||||
hflip H-Flip
|
||||
index Index
|
||||
map Map
|
||||
palette Palette
|
||||
scale Scale
|
||||
title BG Maps
|
||||
vflip V-Flip
|
||||
}
|
||||
|
||||
# Breakpoints window
|
||||
breakpoints {
|
||||
address Address
|
||||
|
|
2
makefile
2
makefile
|
@ -9,7 +9,7 @@ default:
|
|||
@echo $(include_linux)
|
||||
@echo "Planet Virtual Boy Emulator"
|
||||
@echo " https://www.planetvb.com/"
|
||||
@echo " December 25, 2020"
|
||||
@echo " December 26, 2020"
|
||||
@echo
|
||||
@echo "Intended build environment: Debian i386 or amd64"
|
||||
@echo " gcc-multilib"
|
||||
|
|
|
@ -55,8 +55,12 @@ public class App {
|
|||
fntMono = new Util.Font(new Font(Util.fontFamily(new String[]
|
||||
{ "Consolas", Font.MONOSPACED } ), Font.PLAIN, 14));
|
||||
hexDigitWidth = hexDigitWidth();
|
||||
rgbBase = new int[][] { GREEN, MAGENTA, RED };
|
||||
rgbClear = 0x003050;
|
||||
rgbBase = new int[][] {
|
||||
Arrays.copyOf(GREEN , 4),
|
||||
Arrays.copyOf(MAGENTA, 4),
|
||||
Arrays.copyOf(RED , 4)
|
||||
};
|
||||
rgbClear = 0x555555;
|
||||
|
||||
// Additional processing
|
||||
setUseNative(useNative);
|
||||
|
|
|
@ -0,0 +1,512 @@
|
|||
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;
|
||||
}
|
||||
|
||||
}
|
|
@ -3,6 +3,7 @@ package app;
|
|||
// Java imports
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.image.*;
|
||||
import java.util.*;
|
||||
import javax.swing.*;
|
||||
|
||||
|
@ -18,12 +19,15 @@ class CharactersWindow extends ChildWindow {
|
|||
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
|
||||
|
@ -35,7 +39,6 @@ class CharactersWindow extends ChildWindow {
|
|||
private JSpinner spnWide; // Wide spinner
|
||||
private JTextField txtAddress; // Address text box
|
||||
private JTextField txtMirror; // Mirror text box
|
||||
private JComboBox<String> cmbPalette; // Palette drop-down
|
||||
|
||||
|
||||
|
||||
|
@ -50,6 +53,8 @@ class CharactersWindow extends ChildWindow {
|
|||
// 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
|
||||
|
@ -77,8 +82,8 @@ class CharactersWindow extends ChildWindow {
|
|||
// Character controls
|
||||
label(ctrls, "characters.index", true);
|
||||
spnIndex = spinner(ctrls, 0, 2047, 0, true);
|
||||
spnIndex.addChangeListener(e->{
|
||||
setIndex((Integer) spnIndex.getValue()); });
|
||||
spnIndex.addChangeListener(e->
|
||||
setIndex((Integer) spnIndex.getValue()));
|
||||
label(ctrls, "characters.address", false);
|
||||
txtAddress = textBox(ctrls);
|
||||
txtAddress.addFocusListener(Util.onFocus(null,
|
||||
|
@ -159,7 +164,9 @@ class CharactersWindow extends ChildWindow {
|
|||
panCharacters = new UPanel();
|
||||
panCharacters.setBackground(SystemColor.control);
|
||||
panCharacters.addMouseListener(
|
||||
Util.onMouse(e->onMouseDownCharacters(e), null));
|
||||
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);
|
||||
|
@ -167,6 +174,7 @@ class CharactersWindow extends ChildWindow {
|
|||
// Configure component
|
||||
setContentPane(client);
|
||||
setIndex(0);
|
||||
onView();
|
||||
pack();
|
||||
}
|
||||
|
||||
|
@ -178,7 +186,17 @@ class CharactersWindow extends ChildWindow {
|
|||
|
||||
// Update the display
|
||||
void refresh() {
|
||||
repaint();
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
||||
|
@ -217,14 +235,27 @@ class CharactersWindow extends ChildWindow {
|
|||
setIndex(index);
|
||||
}
|
||||
|
||||
// Characters mouse button press
|
||||
private void onMouseDownCharacters(MouseEvent e) {
|
||||
// Characters mouse
|
||||
private void onMouseCharacters(MouseEvent e) {
|
||||
int id = e.getID();
|
||||
|
||||
// Common processing
|
||||
client.requestFocus();
|
||||
// Mouse release
|
||||
if (id == MouseEvent.MOUSE_RELEASED) {
|
||||
if (e.getButton() == MouseEvent.BUTTON1)
|
||||
dragging = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Only consider left clicks
|
||||
if (e.getButton() != MouseEvent.BUTTON1)
|
||||
// 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
|
||||
|
@ -278,7 +309,11 @@ class CharactersWindow extends ChildWindow {
|
|||
|
||||
// Pattern mouse
|
||||
private void onMousePattern(MouseEvent e) {
|
||||
int id = e.getID();
|
||||
int id = e.getID();
|
||||
int scale = Math.max(1, Math.min(
|
||||
panPattern.getWidth () >> 3,
|
||||
panPattern.getHeight() >> 3
|
||||
));
|
||||
|
||||
// Mouse release
|
||||
if (id == MouseEvent.MOUSE_RELEASED) {
|
||||
|
@ -289,109 +324,60 @@ class CharactersWindow extends ChildWindow {
|
|||
|
||||
// Mouse press
|
||||
if (id == MouseEvent.MOUSE_PRESSED) {
|
||||
client.requestFocus();
|
||||
if (e.getButton() != MouseEvent.BUTTON1)
|
||||
return;
|
||||
dragging = e.getPoint();
|
||||
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;
|
||||
|
||||
// Configure working variables
|
||||
var pix = parent.chrs[index];
|
||||
var pos = e.getPoint();
|
||||
int scale = Math.max(1, Math.min(
|
||||
panPattern.getWidth() / 8, panPattern.getHeight() / 8));
|
||||
// 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);
|
||||
|
||||
// Determine the bounds of the line segment
|
||||
int left = Math.min(dragging.x, pos.x) / scale;
|
||||
int right = (Math.max(dragging.x, pos.x) - 1) / scale;
|
||||
int top = Math.min(dragging.y, pos.y) / scale;
|
||||
int bottom = (Math.max(dragging.y, pos.y) - 1) / scale;
|
||||
if (left >= 8 || right < 0 || top >= 8 || bottom < 0) {
|
||||
dragging = pos;
|
||||
return;
|
||||
// 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; }
|
||||
}
|
||||
|
||||
// The line segment occupies a single column of pixels
|
||||
if (left == right) {
|
||||
top = Math.max(0, top );
|
||||
bottom = Math.min(7, bottom);
|
||||
|
||||
// Draw the column
|
||||
for (
|
||||
int y = top, dest = left + top * 8;
|
||||
y <= bottom;
|
||||
y++, dest += 8
|
||||
) pix[dest] = (byte) color;
|
||||
|
||||
// Update the current VRAM state
|
||||
parent.encode(index);
|
||||
dragging = pos;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate and order the vertex coordinates
|
||||
float v0x = (float) dragging.x / scale;
|
||||
float v0y = (float) dragging.y / scale;
|
||||
float v1x = (float) pos .x / scale;
|
||||
float v1y = (float) pos .y / scale;
|
||||
if (v0x > v1x) {
|
||||
float t = v0x; v0x = v1x; v1x = t;
|
||||
t = v0y; v0y = v1y; v1y = t;
|
||||
}
|
||||
|
||||
// Determine the starting position of the line segment
|
||||
float slope = (v1y - v0y) / (v1x - v0x);
|
||||
float cur, next;
|
||||
if (v0x < 0) {
|
||||
left = 0;
|
||||
cur = v0y - slope * v0x;
|
||||
next = cur + slope;
|
||||
} else {
|
||||
cur = v0y;
|
||||
next = v0y + slope * (1 - v0x % 1);
|
||||
}
|
||||
|
||||
// Draw all columns of pixels
|
||||
int last = Math.min(7, right);
|
||||
for (int x = left; x <= last; x++) {
|
||||
|
||||
// The column is the final column in the line segment
|
||||
if (x == right)
|
||||
next = v1y;
|
||||
|
||||
// Determine the top and bottom rows of pixels
|
||||
v0y = Math.max(cur, next);
|
||||
if (v0y % 1 == 0)
|
||||
v0y--;
|
||||
top = Math.max(0, (int) Math.floor(Math.min(cur, next)));
|
||||
bottom = Math.min(7, (int) Math.floor(v0y ));
|
||||
|
||||
// Draw the column
|
||||
for (
|
||||
int y = top, dest = x + top * 8;
|
||||
y <= bottom;
|
||||
y++, dest += 8
|
||||
) pix[dest] = (byte) color;
|
||||
|
||||
// Advance to the next column
|
||||
cur = next;
|
||||
next += slope;
|
||||
}
|
||||
|
||||
// Update the current VRAM state
|
||||
parent.encode(index);
|
||||
dragging = pos;
|
||||
// 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) {
|
||||
var clear = new Color(parent.app.rgbClear);
|
||||
int grid = this.grid ? 1 : 0;
|
||||
var pal = parent.palettes[palette][MainWindow.RED];
|
||||
var pix = new byte[64];
|
||||
int size = scale * 8 + grid;
|
||||
int wide = this.wide != 0 ? this.wide :
|
||||
Math.max(1, (width + grid) / size);
|
||||
|
@ -402,9 +388,12 @@ class CharactersWindow extends ChildWindow {
|
|||
for (int X = 0, x = 0; x < wide; x++, z++, X += size) {
|
||||
if (z == 2048)
|
||||
break;
|
||||
g.setColor(clear);
|
||||
g.fillRect(X, Y, size - grid, size - grid);
|
||||
drawCharacter(g, X, Y, scale, z, pal);
|
||||
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
|
||||
|
@ -438,7 +427,7 @@ class CharactersWindow extends ChildWindow {
|
|||
}
|
||||
|
||||
// Draw the color area
|
||||
g.setColor(x == 0 ? new Color(parent.app.rgbClear) : pal[x]);
|
||||
g.setColor(new Color(pal[x]));
|
||||
g.fillRect(left + 3, top + 3, size , size );
|
||||
}
|
||||
|
||||
|
@ -449,10 +438,12 @@ class CharactersWindow extends ChildWindow {
|
|||
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;
|
||||
g.setColor(new Color(parent.app.rgbClear));
|
||||
g.fillRect(x, y, scale * 8, scale * 8);
|
||||
drawCharacter(g, x, y, scale, index,
|
||||
parent.palettes[palette][MainWindow.RED]);
|
||||
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
|
||||
|
@ -515,6 +506,7 @@ class CharactersWindow extends ChildWindow {
|
|||
wide * (scale * 8 + grid) - grid,
|
||||
(2047 + wide) / wide * (scale * 8 + grid) - grid
|
||||
));
|
||||
scrCharacters.getVerticalScrollBar().setUnitIncrement(8*scale+grid);
|
||||
panCharacters.revalidate();
|
||||
panCharacters.repaint();
|
||||
}
|
||||
|
@ -538,19 +530,6 @@ class CharactersWindow extends ChildWindow {
|
|||
return chk;
|
||||
}
|
||||
|
||||
// Draw a character
|
||||
private void drawCharacter(Graphics2D g, int X, int Y, int scale,
|
||||
int index, Color[] pal) {
|
||||
var pix = parent.chrs[index];
|
||||
for (int y = 0, src = 0; y < 8; y++)
|
||||
for (int x = 0; x < 8; x++, src++) {
|
||||
if (pix[src] == 0)
|
||||
continue;
|
||||
g.setColor(pal[pix[src]]);
|
||||
g.fillRect(X + x * scale, Y + y * scale, scale, scale);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a label to the controls panel
|
||||
private void label(UPanel panel, String key, boolean top) {
|
||||
var lbl = new JLabel();
|
||||
|
@ -562,14 +541,14 @@ class CharactersWindow extends ChildWindow {
|
|||
}
|
||||
|
||||
// Add a combo box to the controls panel
|
||||
private JComboBox<String> select(UPanel panel, String[] options) {
|
||||
var cmb = new JComboBox<String>();
|
||||
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, 1, 2);
|
||||
gbc.insets = new Insets(0, 0, 2, 2);
|
||||
panel.add(cmb, gbc);
|
||||
return cmb;
|
||||
}
|
||||
|
|
|
@ -15,12 +15,11 @@ import vue.*;
|
|||
class MainWindow extends JFrame {
|
||||
|
||||
// Instance fields
|
||||
App app; // Containing application
|
||||
Breakpoint brkStep; // Single step internal breakpoint
|
||||
byte[][] chrs; // Decoded pixel patterns
|
||||
Color[][][] palettes; // Raster palettes
|
||||
byte[] vram; // Snapshot of VIP memory
|
||||
Vue vue; // Emulation core context
|
||||
App app; // Containing application
|
||||
Breakpoint brkStep; // Single step internal breakpoint
|
||||
int[][][] palettes; // Raster palettes
|
||||
byte[] vram; // Snapshot of VIP memory
|
||||
Vue vue; // Emulation core context
|
||||
|
||||
// Private fields
|
||||
private boolean debugMode; // Window is in debug mode
|
||||
|
@ -39,6 +38,7 @@ class MainWindow extends JFrame {
|
|||
private JMenuItem mnuFileGameMode; // File -> Game mode
|
||||
|
||||
// Child windows
|
||||
private BGMapsWindow bgMaps;
|
||||
private BreakpointsWindow breakpoints;
|
||||
private CharactersWindow characters;
|
||||
private ConsoleWindow console;
|
||||
|
@ -85,21 +85,13 @@ class MainWindow extends JFrame {
|
|||
|
||||
// Configure instance fields
|
||||
this.app = app;
|
||||
chrs = new byte[2048][64];
|
||||
palettes = new Color[9][3][4];
|
||||
palettes = new int[9][3][4];
|
||||
pwd = Util.PWD;
|
||||
vram = new byte[0x40000];
|
||||
vue = Vue.create(app.getUseNative());
|
||||
System.out.println("Native: " +
|
||||
(vue.isNative() ? Vue.getNativeID() : "No"));
|
||||
|
||||
// Initialize palettes
|
||||
var invis = new Color(0, true);
|
||||
for (int x = 0; x < 9; x++)
|
||||
for (int y = 0; y < 3; y++)
|
||||
for (int z = 0; z < 4; z++)
|
||||
palettes[x][y][z] = invis;
|
||||
|
||||
// Configure video pane
|
||||
video = new UPanel();
|
||||
video.setPreferredSize(new Dimension(384, 224));
|
||||
|
@ -121,6 +113,7 @@ class MainWindow extends JFrame {
|
|||
// Configure child windows
|
||||
desktop = new JDesktopPane();
|
||||
desktop.setBackground(SystemColor.controlShadow);
|
||||
desktop.add(bgMaps = new BGMapsWindow (this));
|
||||
desktop.add(breakpoints = new BreakpointsWindow(this));
|
||||
desktop.add(characters = new CharactersWindow (this));
|
||||
desktop.add(console = new ConsoleWindow (this));
|
||||
|
@ -181,10 +174,10 @@ class MainWindow extends JFrame {
|
|||
|
||||
mnuDebug.addSeparator();
|
||||
|
||||
var mnuDebugBackgrounds = new JMenuItem();
|
||||
mnuDebugBackgrounds.setEnabled(false);
|
||||
loc.add(mnuDebugBackgrounds, "app.debug.backgrounds");
|
||||
mnuDebug.add(mnuDebugBackgrounds);
|
||||
var mnuDebugBGMaps = new JMenuItem();
|
||||
loc.add(mnuDebugBGMaps, "app.debug.bg_maps");
|
||||
mnuDebugBGMaps.addActionListener(e->bgMaps.setVisible(true));
|
||||
mnuDebug.add(mnuDebugBGMaps);
|
||||
|
||||
var mnuDebugCharacters = new JMenuItem();
|
||||
loc.add(mnuDebugCharacters, "app.debug.characters");
|
||||
|
@ -251,28 +244,32 @@ class MainWindow extends JFrame {
|
|||
// Package Methods //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Encode a character graphic
|
||||
void encode(int index) {
|
||||
int dest = index >> 9 << 15 | 0x00006000 | (index & 511) << 4;
|
||||
var pix = chrs[index];
|
||||
for (int y = 0, src = 0; y < 8; y++)
|
||||
for (int b = 0, bits = 0; b < 2; b++, vram[dest++] = (byte) bits)
|
||||
for (int x = 0; x < 4; x++, src++)
|
||||
bits = bits >> 2 | pix[src] << 6;
|
||||
vue.writeBytes(dest - 16, pix, dest - 16, 16);
|
||||
refreshDebugLite(false);
|
||||
// Draw a character into a pixel buffer
|
||||
void drawCharacter(int index, boolean hFlip, boolean vFlip, int[] palette,
|
||||
int[] buffer, int offset, int stride) {
|
||||
int addr = index >> 9 << 15 | 0x00006000 | (index & 511) << 4;
|
||||
|
||||
for (int y = 0; y < 8; y++, offset += stride - 8)
|
||||
for (int x = 0; x < 8; x++, offset += 1 ) {
|
||||
int ix = hFlip ? 7 - x : x;
|
||||
int iy = vFlip ? 7 - y : y;
|
||||
int b = addr | iy << 1 | ix >> 2;
|
||||
int bit = (ix & 3) << 1;
|
||||
buffer[offset] = palette[vram[b] >> bit & 3];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Refresh all debug views
|
||||
void refreshDebug(boolean seekToPC) {
|
||||
vue.readBytes(0x00000000, vram, 0, vram.length);
|
||||
refreshCharacters();
|
||||
refreshPalettes();
|
||||
refreshDebugLite(seekToPC);
|
||||
}
|
||||
|
||||
// Refresh all debug views without retrieving video memory
|
||||
void refreshDebugLite(boolean seekToPC) {
|
||||
bgMaps .refresh();
|
||||
breakpoints.refresh();
|
||||
characters .refresh();
|
||||
cpu .refresh(seekToPC);
|
||||
|
@ -412,18 +409,6 @@ class MainWindow extends JFrame {
|
|||
// Private Methods //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Update the character patterns
|
||||
private void refreshCharacters() {
|
||||
for (int index = 0; index < 2048; index++) {
|
||||
var pix = chrs[index];
|
||||
int src = index >> 9 << 15 | 0x00006000 | (index & 511) << 4;
|
||||
for (int y = 0, dest = 0; y < 8; y++)
|
||||
for (int b = 0; b < 2; b++, src++)
|
||||
for (int x = 0, bits = vram[src]; x < 4; x++, dest++, bits >>= 2)
|
||||
pix[dest] = (byte) (bits & 3);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the palette composites
|
||||
private void refreshPalettes() {
|
||||
|
||||
|
@ -460,11 +445,11 @@ class MainWindow extends JFrame {
|
|||
for (int y = 0; y < 3; y++) {
|
||||
var base = app.rgbBase[y];
|
||||
var dest = palettes[x][y];
|
||||
dest[0] = 0xFF000000 | app.rgbClear;
|
||||
for (int z = 1; z < 4; z++) {
|
||||
int argb = 0xFF000000;
|
||||
dest[z] = 0xFF000000;
|
||||
for (int w = 0, bits = 16; w < 3; w++, bits -= 8)
|
||||
argb |= (pal[z] * base[w] + 255) / 510 << bits;
|
||||
dest[z] = new Color(argb);
|
||||
dest[z] |= (pal[z] * base[w] + 255) / 510 << bits;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
package util;
|
||||
|
||||
// Java imports
|
||||
import java.awt.event.*;
|
||||
import java.util.*;
|
||||
import javax.swing.*;
|
||||
|
||||
// Wrapper around JComboBox<String> to enforce desired behaviors
|
||||
public class UComboBox extends JComboBox<String> {
|
||||
|
||||
// Instance fields
|
||||
private HashMap<ActionListener, ActionListener> actionListeners;
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Constants //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Empty item set
|
||||
private static final String[] EMPTY = new String[0];
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Constructors //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Default constructor
|
||||
public UComboBox() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
// List constructor
|
||||
public UComboBox(String[] items) {
|
||||
super();
|
||||
actionListeners = new HashMap<ActionListener, ActionListener>();
|
||||
setItems(items);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Public Methods //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Add a listener to receive action events
|
||||
public void addActionListener(ActionListener l) {
|
||||
|
||||
// Error checking
|
||||
if (l == null)
|
||||
return;
|
||||
|
||||
// Wrap the event listener in a guard
|
||||
ActionListener L = e->{
|
||||
|
||||
// Search the stack trace for an invocation not from Java itself
|
||||
var trace = Thread.currentThread().getStackTrace();
|
||||
for (int x = 2; x < trace.length; x++)
|
||||
if (trace[x].getModuleName() == null)
|
||||
return;
|
||||
|
||||
// Call the event handler
|
||||
l.actionPerformed(e);
|
||||
};
|
||||
|
||||
// Manage the collections
|
||||
actionListeners.put(l, L);
|
||||
super.addActionListener(L);
|
||||
}
|
||||
|
||||
// Retrieve a list of the current action listeners
|
||||
public ActionListener[] getActionListeners() {
|
||||
return actionListeners.keySet().toArray(
|
||||
new ActionListener[actionListeners.size()]);
|
||||
}
|
||||
|
||||
// Remove an action listener from the collection
|
||||
public void removeActionListener(ActionListener l) {
|
||||
var L = actionListeners.get(l);
|
||||
if (L == null)
|
||||
return;
|
||||
actionListeners.remove(l);
|
||||
super.removeActionListener(L);
|
||||
}
|
||||
|
||||
// Update the items in the list
|
||||
public void setItems(String[] items) {
|
||||
int index = getSelectedIndex();
|
||||
setModel(new DefaultComboBoxModel<String>(
|
||||
items != null ? items : EMPTY));
|
||||
setSelectedIndex(index);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue