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