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; } }