package app; // Java imports import java.awt.*; import java.awt.event.*; 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 scale; // Display scale private int wide; // Number of characters per row // UI components private JCheckBox chkGrid; // Grid check box private UPanel client; // Client area 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 private JComboBox cmbPalette; // Palette drop-down /////////////////////////////////////////////////////////////////////////// // Constructors // /////////////////////////////////////////////////////////////////////////// // Default constructor CharactersWindow(MainWindow parent) { super(parent, "characters.title"); // Configure instance fields color = 0; grid = true; 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->onMouseDownCharacters(e), null)); panCharacters.addPaintListener((g,w,h)->onPaintCharacters(g, w, h)); scrCharacters = new JScrollPane(panCharacters); client.add(scrCharacters, BorderLayout.CENTER); // Configure component setContentPane(client); setIndex(0); pack(); } /////////////////////////////////////////////////////////////////////////// // Package Methods // /////////////////////////////////////////////////////////////////////////// // Update the display void refresh() { 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 button press private void onMouseDownCharacters(MouseEvent e) { // Common processing client.requestFocus(); // Only consider left clicks if (e.getButton() != MouseEvent.BUTTON1) 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(); // Mouse release if (id == MouseEvent.MOUSE_RELEASED) { if (e.getButton() == MouseEvent.BUTTON1) dragging = null; return; } // Mouse press if (id == MouseEvent.MOUSE_PRESSED) { if (e.getButton() != MouseEvent.BUTTON1) return; dragging = e.getPoint(); } // 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)); // 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; } // 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; } // 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); 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; g.setColor(clear); g.fillRect(X, Y, size - grid, size - grid); drawCharacter(g, X, Y, scale, z, pal); } // 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(x == 0 ? new Color(parent.app.rgbClear) : 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; 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]); } // 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 )); 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; } // 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(); 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 JComboBox select(UPanel panel, String[] options) { var cmb = new JComboBox(); 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); 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; } }