A downloadable notespace code

Download NowName your own price

/*Copyright (C) 2026 by damijin

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.*/

package jinpad;

import javax.imageio.ImageIO;

import javax.swing.*;

import javax.swing.event.DocumentEvent;

import javax.swing.event.DocumentListener;

import javax.swing.table.AbstractTableModel;

import java.awt.*;

import java.awt.event.*;

import java.awt.datatransfer.DataFlavor;

import java.awt.image.BufferedImage;

import java.awt.geom.*;

import java.io.*;

import java.nio.charset.StandardCharsets;

import java.nio.file.Files;

import java.util.Properties;

import java.util.List;

import java.util.ArrayList;

import java.util.regex.*;

import java.util.zip.*;

/** A two-layer notepad: editable text below a transparent raster drawing. */

public final class JinPad extends JFrame {

private final JTextArea text = new JTextArea();

private final DrawingCanvas canvas = new DrawingCanvas();

private final JLayeredPane paper = new JLayeredPane();

private final JScrollPane scroll = new JScrollPane(paper);

private final JComboBox<String> fontBox = new JComboBox<>(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames());

private final JSpinner sizeBox = new JSpinner(new SpinnerNumberModel(16, 8, 96, 1));

private final JToggleButton drawMode = new JToggleButton("✎ Draw mode");

private File currentFile;

public static void main(String[] args) { SwingUtilities.invokeLater(() -> new JinPad().setVisible(true)); }

JinPad() {

super("JinPad — Digital Sketchpad");

setDefaultCloseOperation(EXIT_ON_CLOSE); setSize(1000, 700); setLocationByPlatform(true);

text.setLineWrap(true); text.setWrapStyleWord(true); text.setFont(new Font("Segoe UI", Font.PLAIN, 16));

// The paper supplies the white background. Keeping the text transparent lets

// the ghosted sketch show through in typing mode without blocking clicks.

text.setOpaque(false);

text.setBorder(BorderFactory.createEmptyBorder(20, 24, 20, 24));

paper.setLayout(null); paper.setBackground(Color.WHITE); paper.setOpaque(true);

paper.add(text, JLayeredPane.DEFAULT_LAYER); paper.add(canvas, JLayeredPane.PALETTE_LAYER);

setDrawingActive(false);

installImageDropTarget(paper); installImageDropTarget(text); installImageDropTarget(canvas);

scroll.setBorder(BorderFactory.createEmptyBorder());

setJMenuBar(menuBar());

add(toolbar(), BorderLayout.NORTH); add(toolsPanel(), BorderLayout.WEST); add(scroll, BorderLayout.CENTER); add(status(), BorderLayout.SOUTH);

scroll.getViewport().addComponentListener(new ComponentAdapter() { public void componentResized(ComponentEvent e) { layoutPaper(); }});

text.getDocument().addDocumentListener(new DocumentListener() { public void insertUpdate(DocumentEvent e) { layoutPaper(); } public void removeUpdate(DocumentEvent e) { layoutPaper(); } public void changedUpdate(DocumentEvent e) { layoutPaper(); }});

fontBox.setSelectedItem("Segoe UI"); applyFont();

layoutPaper();

}

private JComponent toolbar() {

JToolBar bar = new JToolBar(); bar.setFloatable(false); bar.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8));

JButton fresh = button("New", e -> newNote()); JButton open = button("Open", e -> open()); JButton save = button("Save", e -> save(false));

JButton saveAs = button("Save as", e -> save(true));

JCheckBox wrap = new JCheckBox("Word wrap", true); wrap.addActionListener(e -> { text.setLineWrap(wrap.isSelected()); layoutPaper(); });

fontBox.setMaximumSize(new Dimension(190, 28)); styleInput(fontBox); styleInput(sizeBox); fontBox.addActionListener(e -> applyFont()); sizeBox.addChangeListener(e -> applyFont());

styleTool(drawMode); drawMode.addActionListener(e -> { setDrawingActive(drawMode.isSelected()); if (!drawMode.isSelected()) text.requestFocusInWindow(); });

for (Component c : new Component[]{fresh, open, save, saveAs, new JToolBar.Separator(), new JLabel("Font "), fontBox, new JLabel(" Size "), sizeBox, wrap, new JToolBar.Separator(), drawMode}) bar.add(c);

return bar;

}

private JComponent toolsPanel() {

JPanel rail = new JPanel(); rail.setLayout(new BoxLayout(rail, BoxLayout.Y_AXIS)); rail.setBackground(new Color(244, 247, 250)); rail.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, new Color(210, 216, 224)), BorderFactory.createEmptyBorder(10, 8, 10, 8)));

JLabel title = new JLabel("TOOLS"); title.setFont(title.getFont().deriveFont(Font.BOLD, 10f)); title.setForeground(new Color(93, 103, 116)); title.setAlignmentX(Component.CENTER_ALIGNMENT); rail.add(title); rail.add(Box.createVerticalStrut(7));

JPanel tools = new JPanel(new GridLayout(0, 2, 4, 4)); tools.setOpaque(false);

ButtonGroup group = new ButtonGroup();

JToggleButton type = toolButton("T", "Text / type", null); type.setSelected(true); type.addActionListener(e -> { drawMode.setSelected(false); setDrawingActive(false); text.requestFocusInWindow(); }); group.add(type); tools.add(type);

for (Object[] spec : new Object[][]{{"⌖", "Select / move imported art", DrawingCanvas.Tool.SELECT}, {"▤", "Kanban card", DrawingCanvas.Tool.CARD}, {"✎", "Pencil", DrawingCanvas.Tool.PENCIL}, {"▰", "Brush", DrawingCanvas.Tool.BRUSH}, {"╱", "Line", DrawingCanvas.Tool.LINE}, {"□", "Rectangle", DrawingCanvas.Tool.RECTANGLE}, {"▢", "Rounded rectangle", DrawingCanvas.Tool.ROUND_RECT}, {"○", "Ellipse", DrawingCanvas.Tool.ELLIPSE}, {"★", "Star stamp", DrawingCanvas.Tool.STAR}, {"♥", "Heart stamp", DrawingCanvas.Tool.HEART}, {"◆", "Diamond stamp", DrawingCanvas.Tool.DIAMOND}, {"➜", "Arrow stamp", DrawingCanvas.Tool.ARROW}, {"☁", "Cloud stamp", DrawingCanvas.Tool.CLOUD}}) { JToggleButton b = toolButton((String) spec[0], (String) spec[1], (DrawingCanvas.Tool) spec[2]); b.addActionListener(e -> activateTool((DrawingCanvas.Tool) spec[2])); group.add(b); tools.add(b); }

rail.add(tools); rail.add(Box.createVerticalStrut(12));

JToggleButton table = toolButton("<html><center><span style='font-size:26px'>▦</span><br>TABLE</center></html>", "Light spreadsheet table", DrawingCanvas.Tool.TABLE); table.setFont(table.getFont().deriveFont(Font.BOLD, 14f)); table.addActionListener(e -> activateTool(DrawingCanvas.Tool.TABLE)); group.add(table); JPanel tableRow = new JPanel(new GridLayout(1, 1)); tableRow.setOpaque(false); tableRow.setMaximumSize(new Dimension(Integer.MAX_VALUE, 84)); tableRow.setPreferredSize(new Dimension(290, 84)); tableRow.add(table); rail.add(tableRow); rail.add(Box.createVerticalStrut(4));

JLabel ink = new JLabel("INK"); ink.setFont(ink.getFont().deriveFont(Font.BOLD, 10f)); ink.setForeground(new Color(93, 103, 116)); ink.setAlignmentX(Component.CENTER_ALIGNMENT); rail.add(ink); rail.add(Box.createVerticalStrut(5));

JPanel swatches = new JPanel(new GridLayout(0, 4, 4, 4)); swatches.setOpaque(false);

Color[] colors = {new Color(38, 43, 51), new Color(95, 105, 120), new Color(49, 96, 184), new Color(121, 80, 183), new Color(0, 174, 185), new Color(78, 184, 81), new Color(167, 205, 69), new Color(244, 183, 47), new Color(246, 137, 50), new Color(239, 104, 79), new Color(210, 67, 109), new Color(153, 81, 54), new Color(255, 112, 147), new Color(119, 203, 242), new Color(235, 240, 245), new Color(255, 255, 255)};

for (Color c : colors) { JButton swatch = new JButton(); styleSwatch(swatch, c); swatch.setToolTipText("Use this ink color"); swatch.addActionListener(e -> canvas.color = c); swatches.add(swatch); }

swatches.setMaximumSize(new Dimension(190, 142)); swatches.setPreferredSize(new Dimension(190, 142)); swatches.setAlignmentX(Component.CENTER_ALIGNMENT);

rail.add(swatches); rail.add(Box.createVerticalStrut(10));

JButton custom = button("Custom color…", e -> { Color c = JColorChooser.showDialog(this, "Ink color", canvas.color); if (c != null) canvas.color = c; }); custom.setAlignmentX(Component.CENTER_ALIGNMENT); rail.add(custom);

rail.add(Box.createVerticalStrut(10)); JLabel size = new JLabel("BRUSH SIZE"); size.setFont(size.getFont().deriveFont(Font.BOLD, 10f)); size.setForeground(new Color(93, 103, 116)); size.setAlignmentX(Component.CENTER_ALIGNMENT); rail.add(size);

JSlider stroke = new JSlider(1, 24, 3); stroke.setPaintTicks(true); stroke.setMinorTickSpacing(1); stroke.setMaximumSize(new Dimension(190, 36)); stroke.setPreferredSize(new Dimension(190, 36)); stroke.setAlignmentX(Component.CENTER_ALIGNMENT); stroke.addChangeListener(e -> canvas.setStroke(stroke.getValue())); rail.add(stroke);

rail.add(Box.createVerticalStrut(10)); JLabel actions = new JLabel("ACTIONS"); actions.setFont(actions.getFont().deriveFont(Font.BOLD, 10f)); actions.setForeground(new Color(93, 103, 116)); actions.setAlignmentX(Component.CENTER_ALIGNMENT); rail.add(actions); rail.add(Box.createVerticalStrut(5));

JPanel actionRow = new JPanel(new GridLayout(1, 2, 5, 0)); actionRow.setOpaque(false); actionRow.setMaximumSize(new Dimension(190, 30)); actionRow.setPreferredSize(new Dimension(190, 30)); actionRow.setAlignmentX(Component.CENTER_ALIGNMENT); JButton undo = button("Undo", e -> canvas.undo()); JButton clear = button("Clear", e -> canvas.clear()); actionRow.add(undo); actionRow.add(clear); rail.add(actionRow);

return rail;

}

private JToggleButton toolButton(String label, String tip, DrawingCanvas.Tool ignored) { JToggleButton b = new JToggleButton(label); b.setToolTipText(tip); b.setFont(b.getFont().deriveFont(Font.BOLD, 18f)); b.setPreferredSize(new Dimension(42, 36)); styleTool(b); return b; }

private void activateTool(DrawingCanvas.Tool tool) { canvas.setTool(tool); drawMode.setSelected(true); setDrawingActive(true); }

private JMenuBar menuBar() {

JMenuBar menus = new JMenuBar();

JMenu file = new JMenu("File"), edit = new JMenu("Edit");

file.add(item("New", KeyStroke.getKeyStroke("control N"), e -> newNote())); file.add(item("Open…", KeyStroke.getKeyStroke("control O"), e -> open())); file.add(item("Save", KeyStroke.getKeyStroke("control S"), e -> save(false))); file.add(item("Save As…", KeyStroke.getKeyStroke("control shift S"), e -> save(true)));

edit.add(item("Undo drawing", KeyStroke.getKeyStroke("control Z"), e -> canvas.undo())); edit.addSeparator(); edit.add(item("Cut", KeyStroke.getKeyStroke("control X"), e -> text.cut())); edit.add(item("Copy", KeyStroke.getKeyStroke("control C"), e -> text.copy())); edit.add(item("Paste", KeyStroke.getKeyStroke("control V"), e -> text.paste())); edit.add(item("Select All", KeyStroke.getKeyStroke("control A"), e -> text.selectAll()));

menus.add(file); menus.add(edit); return menus;

}

private JMenuItem item(String name, KeyStroke key, ActionListener a) { JMenuItem i = new JMenuItem(name); i.setAccelerator(key); i.addActionListener(a); return i; }

private JLabel status() { JLabel l = new JLabel(" Drop PNG, JPG, GIF, or WebP files onto your note. Select an object, then Ctrl+C / Ctrl+V to duplicate it."); l.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0)); return l; }

private JButton button(String name, ActionListener a) { JButton b = new JButton(name); b.addActionListener(a); styleFlat(b, new Color(239, 243, 248)); return b; }

private void styleInput(JComponent control) { control.setBackground(Color.WHITE); control.setForeground(new Color(42, 52, 65)); control.setBorder(BorderFactory.createLineBorder(new Color(183, 198, 214))); if (control instanceof JComboBox<?> combo) combo.setUI(new javax.swing.plaf.basic.BasicComboBoxUI()); if (control instanceof JSpinner spinner) spinner.setUI(new javax.swing.plaf.basic.BasicSpinnerUI()); }

private void styleFlat(AbstractButton b, Color base) { b.setUI(new javax.swing.plaf.basic.BasicButtonUI()); b.setBackground(base); b.setForeground(new Color(42, 52, 65)); b.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(new Color(191, 202, 214)), BorderFactory.createEmptyBorder(5, 8, 5, 8))); b.setFocusPainted(false); b.setContentAreaFilled(true); b.setOpaque(true); b.addChangeListener(e -> b.setBackground(b.getModel().isPressed() ? new Color(205, 218, 233) : base)); }

private void styleTool(JToggleButton b) { b.setUI(new javax.swing.plaf.basic.BasicButtonUI()); b.setForeground(new Color(45, 57, 73)); b.setBorder(BorderFactory.createLineBorder(new Color(202, 211, 222))); b.setFocusPainted(false); b.setRolloverEnabled(true); b.setContentAreaFilled(true); b.setOpaque(true); b.addChangeListener(e -> b.setBackground(b.isSelected() ? new Color(115, 179, 220) : b.getModel().isRollover() ? new Color(231, 239, 247) : Color.WHITE)); b.setBackground(Color.WHITE); }

private void styleSwatch(JButton b, Color color) { b.setUI(new javax.swing.plaf.basic.BasicButtonUI()); b.setBackground(color); b.setBorder(BorderFactory.createLineBorder(color.equals(Color.WHITE) ? new Color(182, 193, 205) : color.darker(), 1)); b.setPreferredSize(new Dimension(31, 31)); b.setFocusPainted(false); b.setContentAreaFilled(true); b.setOpaque(true); }

private void applyFont() { text.setFont(new Font((String) fontBox.getSelectedItem(), Font.PLAIN, (Integer) sizeBox.getValue())); layoutPaper(); }

/** Swap which transparent layer receives mouse input while keeping the artwork visible. */

private void setDrawingActive(boolean active) {

canvas.setDrawingActive(active);

text.setEditable(!active);

paper.setLayer(active ? canvas : text, JLayeredPane.PALETTE_LAYER);

paper.setLayer(active ? text : canvas, JLayeredPane.DEFAULT_LAYER);

canvas.setCursor(active ? Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR) : Cursor.getDefaultCursor());

paper.repaint();

}

private void layoutPaper() {

if (scroll.getViewport() == null) return;

int width = Math.max(300, scroll.getViewport().getWidth());

text.setSize(width, Short.MAX_VALUE); int height = Math.max(scroll.getViewport().getHeight(), text.getPreferredSize().height);

Dimension d = new Dimension(width, height); paper.setPreferredSize(d); paper.setSize(d); text.setBounds(0, 0, width, height); canvas.setBounds(0, 0, width, height); canvas.ensureSize(width, height); paper.revalidate(); paper.repaint();

}

private void save(boolean choose) {

try {

if (choose || currentFile == null) { JFileChooser fc = new JFileChooser(); fc.setDialogTitle("Save JinPad note"); if (fc.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) return; currentFile = fc.getSelectedFile(); if (!currentFile.getName().toLowerCase().endsWith(".jpad")) currentFile = new File(currentFile + ".jpad"); }

Properties p = new Properties(); p.setProperty("font", (String) fontBox.getSelectedItem()); p.setProperty("size", String.valueOf(sizeBox.getValue())); p.setProperty("wrap", String.valueOf(text.getLineWrap()));

p.setProperty("art.count", String.valueOf(canvas.artCount()));

try (ZipOutputStream z = new ZipOutputStream(new FileOutputStream(currentFile))) { entry(z, "note.txt", text.getText().getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream image = new ByteArrayOutputStream(); ImageIO.write(canvas.image(), "png", image); entry(z, "drawing.png", image.toByteArray()); canvas.writeArt(z, p); ByteArrayOutputStream props = new ByteArrayOutputStream(); p.store(props, "JinPad"); entry(z, "settings.properties", props.toByteArray()); }

setTitle("JinPad — " + currentFile.getName());

} catch (IOException ex) { error(ex); }

}

private void newNote() { text.setText(""); canvas.clear(); currentFile = null; setTitle("JinPad — Digital Sketchpad"); text.requestFocusInWindow(); }

private void entry(ZipOutputStream z, String name, byte[] data) throws IOException { z.putNextEntry(new ZipEntry(name)); z.write(data); z.closeEntry(); }

private void open() {

JFileChooser fc = new JFileChooser(); if (fc.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) return;

try (ZipFile z = new ZipFile(fc.getSelectedFile())) { text.setText(new String(z.getInputStream(z.getEntry("note.txt")).readAllBytes(), StandardCharsets.UTF_8)); Properties p = new Properties(); p.load(z.getInputStream(z.getEntry("settings.properties"))); fontBox.setSelectedItem(p.getProperty("font", "Segoe UI")); sizeBox.setValue(Integer.parseInt(p.getProperty("size", "16"))); text.setLineWrap(Boolean.parseBoolean(p.getProperty("wrap", "true"))); canvas.load(ImageIO.read(z.getInputStream(z.getEntry("drawing.png")))); canvas.readArt(z, p); currentFile = fc.getSelectedFile(); applyFont(); setTitle("JinPad — " + currentFile.getName()); } catch (Exception ex) { error(ex); }

}

private void error(Exception e) { JOptionPane.showMessageDialog(this, "Could not read this JinPad file.\n" + e.getMessage(), "JinPad", JOptionPane.ERROR_MESSAGE); }

/** Accepts files dropped from Explorer, including animated GIF files. */

private void installImageDropTarget(JComponent target) {

target.setTransferHandler(new TransferHandler() {

public boolean canImport(TransferSupport support) { return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor); }

public boolean importData(TransferSupport support) {

if (!canImport(support)) return false;

try {

@SuppressWarnings("unchecked") List<File> files = (List<File>) support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);

if (files.isEmpty()) return false;

Point p = SwingUtilities.convertPoint((Component) support.getComponent(), support.getDropLocation().getDropPoint(), paper);

boolean added = false; for (File f : files) if (canvas.addImage(f, p)) { p.translate(18, 18); added = true; }

if (added) { activateTool(DrawingCanvas.Tool.SELECT); } else JOptionPane.showMessageDialog(JinPad.this, "That image format could not be decoded by this Java runtime.\nPNG, JPG, and GIF always work; WebP requires an ImageIO WebP reader.", "JinPad", JOptionPane.INFORMATION_MESSAGE);

return added;

} catch (Exception ex) { error(ex); return false; }

}

});

}

private static final class DrawingCanvas extends JComponent {

enum Tool { SELECT, CARD, TABLE, PENCIL, BRUSH, LINE, RECTANGLE, ROUND_RECT, ELLIPSE, STAR, HEART, DIAMOND, ARROW, CLOUD }

private BufferedImage ink = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB), shadowInk = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); private Point last, start, previewEnd; Color color = new Color(49, 96, 184); private int stroke = 3; private Tool tool = Tool.PENCIL; private boolean drawingActive, resizing; private ArtObject selected, objectClipboard; private final java.util.List<ArtObject> art = new ArrayList<>(); private final java.util.Map<ArtObject, TableData> tables = new java.util.IdentityHashMap<>(); private final java.util.List<Snapshot> history = new ArrayList<>();

DrawingCanvas() { setOpaque(false); setFocusable(true); getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete-selected-art"); getActionMap().put("delete-selected-art", new AbstractAction() { public void actionPerformed(ActionEvent e) { if (selected != null) { art.remove(selected); selected = null; repaint(); } } }); getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke("control C"), "copy-selected-art"); getActionMap().put("copy-selected-art", new AbstractAction() { public void actionPerformed(ActionEvent e) { if (selected != null) objectClipboard = selected.copy(); } }); getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke("control V"), "paste-selected-art"); getActionMap().put("paste-selected-art", new AbstractAction() { public void actionPerformed(ActionEvent e) { pasteObject(); } }); MouseAdapter mouse = new MouseAdapter() {

public void mousePressed(MouseEvent e) { if (!drawingActive) return; requestFocusInWindow(); start = last = e.getPoint(); previewEnd = null; if (tool == Tool.SELECT) { selected = findArt(e.getPoint()); resizing = selected != null && !tables.containsKey(selected) && selected.resizeHandle().contains(e.getPoint()); repaint(); return; } if (tool == Tool.CARD || tool == Tool.TABLE) return; checkpoint(); if (tool == Tool.PENCIL || tool == Tool.BRUSH) dot(last); }

public void mouseDragged(MouseEvent e) { if (!drawingActive) return; if (tool == Tool.SELECT) { if (selected != null) { if (resizing) selected.resizeTo(e.getPoint()); else { selected.x += e.getX() - last.x; selected.y += e.getY() - last.y; } last = e.getPoint(); repaint(); } return; } if (tool == Tool.CARD) { previewEnd = e.getPoint(); repaint(); return; } if (tool == Tool.TABLE) return; if (tool == Tool.PENCIL || tool == Tool.BRUSH) { draw(last, e.getPoint()); last = e.getPoint(); } else { previewEnd = e.getPoint(); repaint(); } }

public void mouseReleased(MouseEvent e) { if (!drawingActive || start == null) return; if (tool == Tool.CARD) { selected = createCard(start, e.getPoint()); if (!editCard(selected)) { art.remove(selected); selected = null; } } else if (tool == Tool.TABLE) { selected = ArtObject.card(start.x, start.y, 1, 1, ""); TableData data = new TableData(); selected.text = encodeTable(data); tables.put(selected, data); refreshTableSize(selected, data); art.add(selected); if (!editTable(selected, data)) { art.remove(selected); tables.remove(selected); selected = null; } } else if (tool != Tool.SELECT && tool != Tool.PENCIL && tool != Tool.BRUSH) { drawTool(ink.createGraphics(), start, e.getPoint()); drawTool(shadowInk.createGraphics(), start, e.getPoint(), Color.BLACK, shadowOffset()); } previewEnd = null; start = null; repaint(); }

public void mouseClicked(MouseEvent e) { if (drawingActive && tool == Tool.SELECT && e.getClickCount() == 2 && selected != null) { if (tables.containsKey(selected)) editTable(selected, tables.get(selected)); else if (selected.kind == ArtObject.Kind.CARD) editCard(selected); } }

}; addMouseListener(mouse); addMouseMotionListener(mouse); }

void setStroke(int n) { stroke = n; } void ensureSize(int w, int h) { if (w <= ink.getWidth() && h <= ink.getHeight()) return; int nw = Math.max(w, ink.getWidth()), nh = Math.max(h, ink.getHeight()); ink = enlarged(ink, nw, nh); shadowInk = enlarged(shadowInk, nw, nh); }

private BufferedImage enlarged(BufferedImage source, int width, int height) { BufferedImage next = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics2D g = next.createGraphics(); g.drawImage(source, 0, 0, null); g.dispose(); return next; }

void setTool(Tool t) { tool = t; } private int inkWidth() { return tool == Tool.PENCIL ? Math.max(1, stroke / 2) : stroke; }

private int shadowOffset() { return Math.max(1, Math.min(6, Math.round(inkWidth() * 0.65f))); }

void draw(Point a, Point b) { strokeLine(ink, a, b, color, 0); strokeLine(shadowInk, a, b, Color.BLACK, shadowOffset()); repaint(); }

private void strokeLine(BufferedImage target, Point a, Point b, Color inkColor, int offset) { Graphics2D g = target.createGraphics(); g.setColor(inkColor); g.setStroke(new BasicStroke(inkWidth(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g.drawLine(a.x + offset, a.y + offset, b.x + offset, b.y + offset); g.dispose(); }

void dot(Point p) { draw(p, p); }

void setDrawingActive(boolean active) { drawingActive = active; repaint(); }

protected void paintComponent(Graphics g) {

super.paintComponent(g);

Graphics2D g2 = (Graphics2D) g.create();

for (ArtObject object : art) { if (!tables.containsKey(object) && object.kind == ArtObject.Kind.CARD && object.text != null && object.text.startsWith("__TABLE__|")) tables.put(object, decodeTable(object.text)); if (tables.containsKey(object)) paintTable(g2, object, tables.get(object), drawingActive ? 1f : .52f); else object.paint(g2, this, drawingActive ? 1f : .52f); }

// Each shadow is drawn with its stroke, preserving its original scaled 45-degree offset.

float inkAlpha = drawingActive ? 1.0f : 0.42f;

g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, drawingActive ? 0.24f : 0.12f)); g2.drawImage(shadowInk, 0, 0, null);

g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, inkAlpha)); g2.drawImage(ink, 0, 0, null); g2.dispose();

if (previewEnd != null && start != null) { Graphics2D preview = (Graphics2D) g.create(); preview.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .7f)); if (tool == Tool.CARD) paintCardPreview(preview, start, previewEnd); else drawTool(preview, start, previewEnd); preview.dispose(); }

if (selected != null && drawingActive) { Graphics2D select = (Graphics2D) g.create(); select.setColor(new Color(33, 123, 192)); select.setStroke(new BasicStroke(1.4f)); select.drawRect(selected.x - 3, selected.y - 3, selected.w + 6, selected.h + 6); if (!tables.containsKey(selected)) { Rectangle h = selected.resizeHandle(); select.setColor(Color.WHITE); select.fill(h); select.setColor(new Color(33, 123, 192)); select.draw(h); } select.dispose(); }

}

private void drawTool(Graphics2D g, Point a, Point b) { drawTool(g, a, b, color, 0); }

private void drawTool(Graphics2D g, Point a, Point b, Color inkColor, int offset) {

g.translate(offset, offset); g.setColor(inkColor); g.setStroke(new BasicStroke(inkWidth(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

int x = Math.min(a.x, b.x), y = Math.min(a.y, b.y), w = Math.abs(a.x - b.x), h = Math.abs(a.y - b.y);

switch (tool) {

case LINE -> g.drawLine(a.x, a.y, b.x, b.y);

case RECTANGLE -> g.drawRect(x, y, w, h);

case ROUND_RECT -> g.drawRoundRect(x, y, w, h, Math.min(28, Math.max(8, w / 3)), Math.min(28, Math.max(8, h / 3)));

case ELLIPSE -> g.drawOval(x, y, w, h);

case STAR -> { Path2D star = new Path2D.Double(); double cx = (a.x + b.x) / 2.0, cy = (a.y + b.y) / 2.0, outer = Math.max(8, Math.min(w, h) / 2.0), inner = outer * .46; for (int i = 0; i < 10; i++) { double angle = -Math.PI / 2 + i * Math.PI / 5; double r = i % 2 == 0 ? outer : inner; if (i == 0) star.moveTo(cx + Math.cos(angle) * r, cy + Math.sin(angle) * r); else star.lineTo(cx + Math.cos(angle) * r, cy + Math.sin(angle) * r); } star.closePath(); g.fill(star); }

case HEART -> { double cx = (a.x + b.x) / 2.0, top = Math.min(a.y, b.y), scale = Math.max(10, Math.min(w, h)); Path2D heart = new Path2D.Double(); heart.moveTo(cx, top + scale); heart.curveTo(cx - scale * 1.3, top + scale * .35, cx - scale * .7, top - scale * .15, cx, top + scale * .28); heart.curveTo(cx + scale * .7, top - scale * .15, cx + scale * 1.3, top + scale * .35, cx, top + scale); heart.closePath(); g.fill(heart); }

case DIAMOND -> { Path2D diamond = new Path2D.Double(); diamond.moveTo((a.x + b.x) / 2.0, y); diamond.lineTo(x + w, (a.y + b.y) / 2.0); diamond.lineTo((a.x + b.x) / 2.0, y + h); diamond.lineTo(x, (a.y + b.y) / 2.0); diamond.closePath(); g.fill(diamond); }

case ARROW -> { double dx = b.x - a.x, dy = b.y - a.y, length = Math.max(1, Math.hypot(dx, dy)); double ux = dx / length, uy = dy / length, px = -uy, py = ux; double head = Math.min(30, Math.max(12, length * .30)), wing = head * .52; g.setStroke(new BasicStroke(Math.max(4, inkWidth() * 1.5f), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g.drawLine(a.x, a.y, (int) Math.round(b.x - ux * head * .52), (int) Math.round(b.y - uy * head * .52)); Path2D arrow = new Path2D.Double(); arrow.moveTo(b.x, b.y); arrow.lineTo(b.x - ux * head + px * wing, b.y - uy * head + py * wing); arrow.lineTo(b.x - ux * head - px * wing, b.y - uy * head - py * wing); arrow.closePath(); g.fill(arrow); }

case CLOUD -> { int d = Math.max(8, Math.min(w, h) / 2); Area cloud = new Area(new Ellipse2D.Double(x, y + h * .35, w, h * .55)); cloud.add(new Area(new Ellipse2D.Double(x + w * .10, y + h * .20, d, d))); cloud.add(new Area(new Ellipse2D.Double(x + w * .34, y, d * 1.2, d * 1.2))); cloud.add(new Area(new Ellipse2D.Double(x + w * .62, y + h * .17, d, d))); g.fill(cloud); }

default -> { }

}

g.dispose();

}

private void paintCardPreview(Graphics2D g, Point a, Point b) { Rectangle r = cardBounds(a, b); g.setColor(new Color(255, 244, 181)); g.fillRoundRect(r.x, r.y, r.width, r.height, 18, 18); g.setColor(new Color(201, 166, 49)); g.drawRoundRect(r.x, r.y, r.width, r.height, 18, 18); }

private Rectangle cardBounds(Point a, Point b) { int x = Math.min(a.x, b.x), y = Math.min(a.y, b.y), w = Math.abs(a.x - b.x), h = Math.abs(a.y - b.y); return new Rectangle(x, y, Math.max(160, w), Math.max(95, h)); }

private ArtObject createCard(Point a, Point b) { Rectangle r = cardBounds(a, b); ArtObject card = ArtObject.card(r.x, r.y, r.width, r.height, "New card"); art.add(card); return card; }

private boolean editCard(ArtObject card) {

JDialog dialog = new JDialog((Frame) SwingUtilities.getWindowAncestor(this), "Card properties", true); JPanel body = new JPanel(new BorderLayout(12, 12)); body.setBackground(new Color(247, 249, 252)); body.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));

JLabel heading = new JLabel("CARD PROPERTIES"); heading.setFont(new Font("Segoe UI", Font.BOLD, 13)); heading.setForeground(new Color(75, 90, 108)); body.add(heading, BorderLayout.NORTH);

JTextArea editor = new JTextArea(card.text, 6, 30); editor.setLineWrap(true); editor.setWrapStyleWord(true); editor.setFont(new Font(card.fontName, Font.PLAIN, card.fontSize)); JScrollPane editorScroll = new JScrollPane(editor); editorScroll.setBorder(BorderFactory.createLineBorder(new Color(190, 202, 216))); body.add(editorScroll, BorderLayout.CENTER);

JPanel controls = new JPanel(new GridLayout(0, 2, 8, 8)); controls.setOpaque(false); JComboBox<String> family = new JComboBox<>(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()); family.setSelectedItem(card.fontName); JSpinner size = new JSpinner(new SpinnerNumberModel(card.fontSize, 8, 72, 1)); JSlider thickness = new JSlider(1, 12, card.outlineThickness); thickness.setPaintTicks(true); thickness.setMinorTickSpacing(1); Color[] textColor = {card.textColor}, fillColor = {card.fillColor}, outlineColor = {card.outlineColor};

controls.add(propertyLabel("Font")); controls.add(family); controls.add(propertyLabel("Text size")); controls.add(size); controls.add(propertyLabel("Text color")); controls.add(colorButton("Text color", textColor)); controls.add(propertyLabel("Card color")); controls.add(colorButton("Card color", fillColor)); controls.add(propertyLabel("Outline color")); controls.add(colorButton("Outline color", outlineColor)); controls.add(propertyLabel("Outline thickness")); controls.add(thickness); body.add(controls, BorderLayout.SOUTH);

final boolean[] accepted = {false}; JPanel actions = new JPanel(new FlowLayout(FlowLayout.RIGHT, 7, 0)); actions.setBackground(new Color(247, 249, 252)); JButton cancel = dialogButton("Cancel"); JButton save = dialogButton("Save card"); cancel.addActionListener(e -> dialog.dispose()); save.addActionListener(e -> { card.text = editor.getText(); card.fontName = (String) family.getSelectedItem(); card.fontSize = (Integer) size.getValue(); card.textColor = textColor[0]; card.fillColor = fillColor[0]; card.outlineColor = outlineColor[0]; card.outlineThickness = thickness.getValue(); accepted[0] = true; repaint(); dialog.dispose(); }); actions.add(cancel); actions.add(save);

JPanel root = new JPanel(new BorderLayout(0, 12)); root.setBackground(new Color(247, 249, 252)); root.add(body, BorderLayout.CENTER); root.add(actions, BorderLayout.SOUTH); dialog.setContentPane(root); dialog.getRootPane().registerKeyboardAction(e -> dialog.dispose(), KeyStroke.getKeyStroke("ESCAPE"), JComponent.WHEN_IN_FOCUSED_WINDOW); dialog.pack(); dialog.setMinimumSize(new Dimension(460, 430)); dialog.setLocationRelativeTo(this); dialog.setVisible(true); return accepted[0];

}

private JLabel propertyLabel(String text) { JLabel label = new JLabel(text); label.setFont(new Font("Segoe UI", Font.BOLD, 12)); label.setForeground(new Color(75, 90, 108)); return label; }

private JButton colorButton(String name, Color[] value) { JButton button = dialogButton(name); button.setBackground(value[0]); button.setForeground(value[0].getRed() + value[0].getGreen() + value[0].getBlue() < 360 ? Color.WHITE : new Color(40, 50, 65)); button.addActionListener(e -> { Color next = JColorChooser.showDialog(this, name, value[0]); if (next != null) { value[0] = next; button.setBackground(next); button.setForeground(next.getRed() + next.getGreen() + next.getBlue() < 360 ? Color.WHITE : new Color(40, 50, 65)); } }); return button; }

private JButton dialogButton(String name) { JButton button = new JButton(name); button.setUI(new javax.swing.plaf.basic.BasicButtonUI()); button.setBackground(new Color(232, 239, 247)); button.setForeground(new Color(42, 52, 65)); button.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(new Color(183, 198, 214)), BorderFactory.createEmptyBorder(6, 10, 6, 10))); button.setFocusPainted(false); button.setOpaque(true); return button; }

private boolean editTable(ArtObject table, TableData data) {

JDialog dialog = new JDialog((Frame) SwingUtilities.getWindowAncestor(this), "Light spreadsheet", true);

JPanel root = new JPanel(new BorderLayout(10, 10)); root.setBackground(new Color(247, 249, 252)); root.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));

JPanel heading = new JPanel(new BorderLayout()); heading.setOpaque(false); JLabel title = new JLabel("LIGHT SPREADSHEET"); title.setFont(new Font("Segoe UI", Font.BOLD, 13)); title.setForeground(new Color(75, 90, 108)); JLabel note = new JLabel("Cell formulas: =A1+B1, =SUM(A1:A3), =AVG(B1:B3) • Reordering is disabled for data safety."); note.setForeground(new Color(93, 103, 116)); heading.add(title, BorderLayout.WEST); heading.add(note, BorderLayout.EAST); root.add(heading, BorderLayout.NORTH);

TableGridModel model = new TableGridModel(data.cells); JTable grid = new JTable(model); grid.setRowHeight(Math.max(26, data.fontSize + 12)); grid.setFont(new Font(data.fontName, Font.PLAIN, data.fontSize)); grid.setGridColor(new Color(205, 213, 223)); grid.setSelectionBackground(new Color(181, 216, 239)); grid.setSelectionForeground(new Color(30, 43, 58)); grid.setCellSelectionEnabled(true); grid.setRowSelectionAllowed(true); grid.setColumnSelectionAllowed(true); grid.setAutoCreateColumnsFromModel(true); grid.getTableHeader().setReorderingAllowed(false); grid.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("control C"), "copy-table-cells"); grid.getActionMap().put("copy-table-cells", new AbstractAction() { public void actionPerformed(ActionEvent e) { copyTableCells(grid); } }); grid.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("control V"), "paste-table-cells"); grid.getActionMap().put("paste-table-cells", new AbstractAction() { public void actionPerformed(ActionEvent e) { pasteTableCells(grid, model); } }); root.add(new JScrollPane(grid), BorderLayout.CENTER);

JButton addRow = dialogButton("+ Row"), removeRow = dialogButton("− Selected Row"), addColumn = dialogButton("+ Column"), removeColumn = dialogButton("− Selected Column"); JSpinner fontSize = new JSpinner(new SpinnerNumberModel(data.fontSize, 8, 40, 1)); JPanel structure = new JPanel(new FlowLayout(FlowLayout.LEFT, 7, 0)); structure.setOpaque(false); structure.add(addRow); structure.add(removeRow); structure.add(addColumn); structure.add(removeColumn); structure.add(new JLabel("Font size")); structure.add(fontSize);

addRow.addActionListener(e -> { stopTableEditing(grid); int row = model.addRow(); grid.setRowSelectionInterval(row, row); }); removeRow.addActionListener(e -> { stopTableEditing(grid); int row = grid.getSelectedRow(); if (row < 0) { Toolkit.getDefaultToolkit().beep(); return; } model.removeRow(row); if (model.getRowCount() > 0) grid.setRowSelectionInterval(Math.min(row, model.getRowCount() - 1), Math.min(row, model.getRowCount() - 1)); }); addColumn.addActionListener(e -> { stopTableEditing(grid); int column = model.addColumn(); grid.createDefaultColumnsFromModel(); grid.setColumnSelectionInterval(column, column); }); removeColumn.addActionListener(e -> { stopTableEditing(grid); int column = grid.getSelectedColumn(); if (column < 0) { Toolkit.getDefaultToolkit().beep(); return; } model.removeColumn(column); grid.createDefaultColumnsFromModel(); if (model.getColumnCount() > 0) grid.setColumnSelectionInterval(Math.min(column, model.getColumnCount() - 1), Math.min(column, model.getColumnCount() - 1)); });

final boolean[] accepted = {false}; JPanel actions = new JPanel(new FlowLayout(FlowLayout.RIGHT, 7, 0)); actions.setOpaque(false); JButton cancel = dialogButton("Cancel"), save = dialogButton("Save table"); cancel.addActionListener(e -> dialog.dispose()); save.addActionListener(e -> { stopTableEditing(grid); data.cells = model.snapshot(); data.fontSize = (Integer) fontSize.getValue(); table.text = encodeTable(data); refreshTableSize(table, data); accepted[0] = true; repaint(); dialog.dispose(); }); actions.add(cancel); actions.add(save); JPanel south = new JPanel(new BorderLayout(0, 8)); south.setOpaque(false); south.add(structure, BorderLayout.CENTER); south.add(actions, BorderLayout.SOUTH); root.add(south, BorderLayout.SOUTH);

dialog.setContentPane(root); dialog.getRootPane().registerKeyboardAction(e -> dialog.dispose(), KeyStroke.getKeyStroke("ESCAPE"), JComponent.WHEN_IN_FOCUSED_WINDOW); dialog.setSize(780, 500); dialog.setLocationRelativeTo(this); dialog.setVisible(true); return accepted[0];

}

private void stopTableEditing(JTable grid) { if (grid.isEditing() && grid.getCellEditor() != null) grid.getCellEditor().stopCellEditing(); }

private void copyTableCells(JTable grid) { int top = grid.getSelectedRow(), left = grid.getSelectedColumn(), bottom = grid.getSelectedRowCount() == 0 ? -1 : top + grid.getSelectedRowCount() - 1, right = grid.getSelectedColumnCount() == 0 ? -1 : left + grid.getSelectedColumnCount() - 1; if (top < 0 || left < 0 || bottom < 0 || right < 0) return; StringBuilder copied = new StringBuilder(); for (int r = top; r <= bottom; r++) { if (r > top) copied.append('\n'); for (int c = left; c <= right; c++) { if (c > left) copied.append('\t'); copied.append(String.valueOf(grid.getValueAt(r, c))); } } Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new java.awt.datatransfer.StringSelection(copied.toString()), null); }

private void pasteTableCells(JTable grid, TableGridModel model) { try { Object value = Toolkit.getDefaultToolkit().getSystemClipboard().getData(java.awt.datatransfer.DataFlavor.stringFlavor); if (value == null) return; String[] pastedRows = String.valueOf(value).replace("\r\n", "\n").split("\n", -1); int startRow = Math.max(0, grid.getSelectedRow()), startColumn = Math.max(0, grid.getSelectedColumn()); int neededRows = startRow + pastedRows.length, neededColumns = startColumn; for (String row : pastedRows) neededColumns = Math.max(neededColumns, startColumn + row.split("\t", -1).length); while (model.getRowCount() < neededRows) model.addRow(); while (model.getColumnCount() < neededColumns) { model.addColumn(); grid.createDefaultColumnsFromModel(); } for (int r = 0; r < pastedRows.length; r++) { String[] cells = pastedRows[r].split("\t", -1); for (int c = 0; c < cells.length; c++) model.setValueAt(cells[c], startRow + r, startColumn + c); } grid.setRowSelectionInterval(startRow, Math.min(neededRows - 1, model.getRowCount() - 1)); grid.setColumnSelectionInterval(startColumn, Math.min(neededColumns - 1, model.getColumnCount() - 1)); } catch (Exception ignored) { Toolkit.getDefaultToolkit().beep(); } }

private void refreshTableSize(ArtObject table, TableData data) { FontMetrics metrics = getFontMetrics(new Font(data.fontName, Font.PLAIN, data.fontSize)); int width = 0; for (int c = 0; c < data.cells[0].length; c++) { int col = 75; for (int r = 0; r < data.cells.length; r++) col = Math.max(col, metrics.stringWidth(data.display(r, c)) + 20); width += col; } table.w = width; table.h = data.cells.length * (metrics.getHeight() + 14) + 1; }

private String encodeTable(TableData data) { StringBuilder result = new StringBuilder("__TABLE__|").append(data.fontName).append('|').append(data.fontSize).append('|').append(data.cells.length).append('|').append(data.cells[0].length); for (String[] row : data.cells) for (String cell : row) result.append('|').append(java.util.Base64.getEncoder().encodeToString(cell.getBytes(StandardCharsets.UTF_8))); return result.toString(); } private TableData decodeTable(String encoded) { try { String[] parts = encoded.split("\\|", -1); TableData data = new TableData(); data.fontName = parts[1]; data.fontSize = Integer.parseInt(parts[2]); int rows = Integer.parseInt(parts[3]), cols = Integer.parseInt(parts[4]); data.cells = new String[rows][cols]; for (int i = 0; i < rows * cols; i++) data.cells[i / cols][i % cols] = new String(java.util.Base64.getDecoder().decode(parts[i + 5]), StandardCharsets.UTF_8); return data; } catch (Exception ex) { return new TableData(); } }

private static final class TableGridModel extends AbstractTableModel {

private final java.util.List<java.util.List<String>> rows = new ArrayList<>(); private int columns;

TableGridModel(String[][] initial) { columns = initial != null && initial.length > 0 && initial[0] != null ? Math.max(1, initial[0].length) : 1; if (initial != null) for (String[] source : initial) { java.util.List<String> row = blankRow(); for (int c = 0; c < Math.min(columns, source.length); c++) row.set(c, source[c] == null ? "" : source[c]); rows.add(row); } if (rows.isEmpty()) rows.add(blankRow()); }

public int getRowCount() { return rows.size(); } public int getColumnCount() { return columns; } public String getColumnName(int column) { StringBuilder name = new StringBuilder(); for (int n = column + 1; n > 0; n = (n - 1) / 26) name.insert(0, (char) ('A' + (n - 1) % 26)); return name.toString(); } public Object getValueAt(int row, int column) { return rows.get(row).get(column); } public boolean isCellEditable(int row, int column) { return true; } public void setValueAt(Object value, int row, int column) { rows.get(row).set(column, value == null ? "" : String.valueOf(value)); fireTableCellUpdated(row, column); }

int addRow() { rows.add(blankRow()); int row = rows.size() - 1; fireTableRowsInserted(row, row); return row; } void removeRow(int row) { if (rows.size() <= 1 || row < 0 || row >= rows.size()) return; rows.remove(row); fireTableRowsDeleted(row, row); } int addColumn() { for (java.util.List<String> row : rows) row.add(""); columns++; fireTableStructureChanged(); return columns - 1; } void removeColumn(int column) { if (columns <= 1 || column < 0 || column >= columns) return; for (java.util.List<String> row : rows) row.remove(column); columns--; fireTableStructureChanged(); }

String[][] snapshot() { String[][] result = new String[rows.size()][columns]; for (int r = 0; r < rows.size(); r++) for (int c = 0; c < columns; c++) result[r][c] = rows.get(r).get(c); return result; } private java.util.List<String> blankRow() { java.util.List<String> row = new ArrayList<>(columns); for (int c = 0; c < columns; c++) row.add(""); return row; }

}

private void paintTable(Graphics2D g, ArtObject table, TableData data, float alpha) { Graphics2D p = (Graphics2D) g.create(); p.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); p.setFont(new Font(data.fontName, Font.PLAIN, data.fontSize)); FontMetrics fm = p.getFontMetrics(); int rowH = fm.getHeight() + 14, x = table.x; p.setColor(new Color(245, 248, 252)); p.fillRect(table.x, table.y, table.w, table.h); for (int c = 0; c < data.cells[0].length; c++) { int colW = 75; for (int r = 0; r < data.cells.length; r++) colW = Math.max(colW, fm.stringWidth(data.display(r, c)) + 20); for (int r = 0; r < data.cells.length; r++) { int y = table.y + r * rowH; p.setColor(Color.WHITE); p.fillRect(x, y, colW, rowH); p.setColor(new Color(178, 191, 207)); p.drawRect(x, y, colW, rowH); p.setColor(new Color(44, 55, 70)); p.drawString(data.display(r, c), x + 10, y + 8 + fm.getAscent()); } x += colW; } p.dispose(); }

private static final class TableData { String[][] cells = {{"Item", "Value", "Total"}, {"Example", "5", "=B2+B2"}, {"Sum", "=SUM(B2:B2)", ""}}; String fontName = "Segoe UI"; int fontSize = 14; String display(int r, int c) { String raw = cells[r][c] == null ? "" : cells[r][c]; if (!raw.startsWith("=")) return raw; try { String e = raw.substring(1).toUpperCase(); Matcher range = Pattern.compile("(SUM|AVG)\\(([A-Z])(\\d+):([A-Z])(\\d+)\\)").matcher(e); if (range.matches()) { int c1 = range.group(2).charAt(0) - 'A', r1 = Integer.parseInt(range.group(3)) - 1, c2 = range.group(4).charAt(0) - 'A', r2 = Integer.parseInt(range.group(5)) - 1; double total = 0; int n = 0; for (int rr = r1; rr <= r2; rr++) for (int cc = c1; cc <= c2; cc++) { total += number(rr, cc); n++; } return numberText("AVG".equals(range.group(1)) ? total / n : total); } Matcher ref = Pattern.compile("([A-Z])(\\d+)").matcher(e); StringBuffer b = new StringBuffer(); while (ref.find()) ref.appendReplacement(b, String.valueOf(number(Integer.parseInt(ref.group(2)) - 1, ref.group(1).charAt(0) - 'A'))); ref.appendTail(b); double total = 0; for (String part : b.toString().split("(?=[+-])")) total += Double.parseDouble(part); return numberText(total); } catch (Exception ex) { return "#ERR"; } } double number(int r, int c) { try { return Double.parseDouble(display(r, c)); } catch (Exception e) { return 0; } } String numberText(double n) { return n == Math.rint(n) ? String.valueOf((long) n) : String.format("%.2f", n); } String encode() { StringBuilder b = new StringBuilder("__TABLE__|").append(fontName).append('|').append(fontSize); for (String[] row : cells) for (String cell : row) b.append('|').append(java.util.Base64.getEncoder().encodeToString(cell.getBytes(StandardCharsets.UTF_8))); return b.toString(); } }

ArtObject findArt(Point p) { for (int i = art.size() - 1; i >= 0; i--) if (art.get(i).bounds().contains(p) || art.get(i).resizeHandle().contains(p)) return art.get(i); return null; }

private void pasteObject() { if (objectClipboard == null) return; selected = objectClipboard.copy(); selected.x += 22; selected.y += 22; art.add(selected); repaint(); }

boolean addImage(File file, Point point) { try { byte[] bytes = Files.readAllBytes(file.toPath()); ArtObject object = new ArtObject(file.getName(), bytes, point.x, point.y, 24, 24); if (object.icon.getIconWidth() < 1 || object.icon.getIconHeight() < 1) return false; double scale = Math.min(1, Math.min(300.0 / object.icon.getIconWidth(), 240.0 / object.icon.getIconHeight())); object.w = Math.max(24, (int) (object.icon.getIconWidth() * scale)); object.h = Math.max(24, (int) (object.icon.getIconHeight() * scale)); art.add(object); selected = object; repaint(); return true; } catch (IOException ex) { return false; } }

int artCount() { return art.size(); } void writeArt(ZipOutputStream z, Properties p) throws IOException { for (int i = 0; i < art.size(); i++) { ArtObject o = art.get(i); String prefix = "art." + i + "."; p.setProperty(prefix + "type", o.kind.name()); p.setProperty(prefix + "x", String.valueOf(o.x)); p.setProperty(prefix + "y", String.valueOf(o.y)); p.setProperty(prefix + "w", String.valueOf(o.w)); p.setProperty(prefix + "h", String.valueOf(o.h)); if (o.kind == ArtObject.Kind.CARD) { p.setProperty(prefix + "text", o.text); p.setProperty(prefix + "font", o.fontName); p.setProperty(prefix + "fontSize", String.valueOf(o.fontSize)); p.setProperty(prefix + "textColor", colorValue(o.textColor)); p.setProperty(prefix + "fillColor", colorValue(o.fillColor)); p.setProperty(prefix + "outlineColor", colorValue(o.outlineColor)); p.setProperty(prefix + "outline", String.valueOf(o.outlineThickness)); } else { String entry = "art/" + i + "-" + o.safeName(); z.putNextEntry(new ZipEntry(entry)); z.write(o.bytes); z.closeEntry(); p.setProperty(prefix + "entry", entry); } } }

void readArt(ZipFile z, Properties p) throws IOException { art.clear(); int count = Integer.parseInt(p.getProperty("art.count", "0")); for (int i = 0; i < count; i++) { String prefix = "art." + i + ".", type = p.getProperty(prefix + "type"); if ("CARD".equals(type)) { ArtObject card = ArtObject.card(Integer.parseInt(p.getProperty(prefix + "x")), Integer.parseInt(p.getProperty(prefix + "y")), Integer.parseInt(p.getProperty(prefix + "w")), Integer.parseInt(p.getProperty(prefix + "h")), p.getProperty(prefix + "text", "")); card.fontName = p.getProperty(prefix + "font", card.fontName); card.fontSize = Integer.parseInt(p.getProperty(prefix + "fontSize", String.valueOf(card.fontSize))); card.textColor = parseColor(p.getProperty(prefix + "textColor"), card.textColor); card.fillColor = parseColor(p.getProperty(prefix + "fillColor"), card.fillColor); card.outlineColor = parseColor(p.getProperty(prefix + "outlineColor"), card.outlineColor); card.outlineThickness = Integer.parseInt(p.getProperty(prefix + "outline", String.valueOf(card.outlineThickness))); art.add(card); } else if (type != null) { ZipEntry entry = z.getEntry(p.getProperty(prefix + "entry")); if (entry != null) art.add(new ArtObject(p.getProperty(prefix + "entry"), z.getInputStream(entry).readAllBytes(), Integer.parseInt(p.getProperty(prefix + "x")), Integer.parseInt(p.getProperty(prefix + "y")), Integer.parseInt(p.getProperty(prefix + "w")), Integer.parseInt(p.getProperty(prefix + "h")))); } else { String[] parts = p.getProperty("art." + i).split("\\|", 5); ZipEntry entry = z.getEntry(parts[0]); if (entry != null) art.add(new ArtObject(parts[0], z.getInputStream(entry).readAllBytes(), Integer.parseInt(parts[1]), Integer.parseInt(parts[2]), Integer.parseInt(parts[3]), Integer.parseInt(parts[4]))); } } selected = null; repaint(); }

private String colorValue(Color color) { return String.format("#%02X%02X%02X", color.getRed(), color.getGreen(), color.getBlue()); } private Color parseColor(String value, Color fallback) { try { return value == null ? fallback : Color.decode(value); } catch (NumberFormatException ex) { return fallback; } }

void checkpoint() { history.add(new Snapshot(copy(ink), copy(shadowInk))); if (history.size() > 30) history.remove(0); } void undo() { if (!history.isEmpty()) { Snapshot last = history.remove(history.size()-1); ink = last.ink; shadowInk = last.shadow; repaint(); } } void clear() { checkpoint(); ink = new BufferedImage(Math.max(1, getWidth()), Math.max(1, getHeight()), BufferedImage.TYPE_INT_ARGB); shadowInk = new BufferedImage(ink.getWidth(), ink.getHeight(), BufferedImage.TYPE_INT_ARGB); art.clear(); selected = null; repaint(); }

BufferedImage image() { return ink; } void load(BufferedImage b) { ink = b == null ? new BufferedImage(1,1,BufferedImage.TYPE_INT_ARGB) : b; shadowInk = new BufferedImage(ink.getWidth(), ink.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g = shadowInk.createGraphics(); g.drawImage(ink, 3, 3, null); g.setComposite(AlphaComposite.SrcIn); g.setColor(Color.BLACK); g.fillRect(0, 0, shadowInk.getWidth(), shadowInk.getHeight()); g.dispose(); history.clear(); repaint(); } private BufferedImage copy(BufferedImage source) { BufferedImage b = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g = b.createGraphics(); g.drawImage(source, 0, 0, null); g.dispose(); return b; } private record Snapshot(BufferedImage ink, BufferedImage shadow) { }

private static final class ArtObject { enum Kind { IMAGE, CARD } final Kind kind; final String name; final byte[] bytes; final ImageIcon icon; String text, fontName = "Segoe UI"; Color textColor = new Color(54, 48, 34), fillColor = new Color(255, 244, 181), outlineColor = new Color(211, 177, 60); int fontSize = 14, outlineThickness = 1; int x, y, w, h; ArtObject(String name, byte[] bytes, int x, int y, int w, int h) { this.kind = Kind.IMAGE; this.name = name; this.bytes = bytes; this.icon = imageIcon(bytes); this.x = x; this.y = y; this.w = w; this.h = h; } private ArtObject(int x, int y, int w, int h, String text) { this.kind = Kind.CARD; this.name = "card"; this.bytes = null; this.icon = null; this.x = x; this.y = y; this.w = w; this.h = h; this.text = text; } static ArtObject card(int x, int y, int w, int h, String text) { return new ArtObject(x, y, w, h, text); } private static ImageIcon imageIcon(byte[] bytes) { ImageIcon direct = new ImageIcon(bytes); if (direct.getIconWidth() > 0) return direct; try { BufferedImage decoded = ImageIO.read(new ByteArrayInputStream(bytes)); return decoded == null ? direct : new ImageIcon(decoded); } catch (IOException ex) { return direct; } } ArtObject copy() { ArtObject copy = kind == Kind.IMAGE ? new ArtObject(name, bytes.clone(), x, y, w, h) : card(x, y, w, h, text); copy.fontName = fontName; copy.fontSize = fontSize; copy.textColor = textColor; copy.fillColor = fillColor; copy.outlineColor = outlineColor; copy.outlineThickness = outlineThickness; return copy; } Rectangle bounds() { return new Rectangle(x, y, w, h); } Rectangle resizeHandle() { return new Rectangle(x + w - 12, y + h - 12, 24, 24); } void resizeTo(Point p) { w = Math.max(kind == Kind.CARD ? 160 : 24, p.x - x); h = Math.max(kind == Kind.CARD ? 95 : 24, p.y - y); } String safeName() { return name.replaceAll("[^a-zA-Z0-9._-]", "_"); } void paint(Graphics2D g, Component observer, float alpha) { Graphics2D art = (Graphics2D) g.create(); art.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); if (kind == Kind.IMAGE) { art.translate(x, y); art.scale(w / (double) icon.getIconWidth(), h / (double) icon.getIconHeight()); icon.paintIcon(observer, art, 0, 0); } else { art.setColor(new Color(40, 45, 55, 55)); art.fillRoundRect(x + 3, y + 4, w, h, 18, 18); art.setColor(fillColor); art.fillRoundRect(x, y, w, h, 18, 18); art.setColor(outlineColor); art.setStroke(new BasicStroke(outlineThickness)); art.drawRoundRect(x + outlineThickness / 2, y + outlineThickness / 2, w - outlineThickness, h - outlineThickness, 18, 18); art.setColor(textColor); art.setFont(new Font(fontName, Font.PLAIN, fontSize)); FontMetrics fm = art.getFontMetrics(); int baseline = y + 14 + fm.getAscent(), maxWidth = w - 24; for (String paragraph : text.split("\\R", -1)) { StringBuilder line = new StringBuilder(); for (String word : paragraph.split(" ", -1)) { String candidate = line.isEmpty() ? word : line + " " + word; if (!line.isEmpty() && fm.stringWidth(candidate) > maxWidth) { if (baseline > y + h - 12) break; art.drawString(line.toString(), x + 12, baseline); baseline += fm.getHeight(); line = new StringBuilder(word); } else line = new StringBuilder(candidate); } if (!line.isEmpty() && baseline <= y + h - 12) { art.drawString(line.toString(), x + 12, baseline); baseline += fm.getHeight(); } } } art.dispose(); } }

}

}

Updated 10 hours ago
Published 2 days ago
StatusReleased
CategoryTool
Authorprotomo
Tagsbsd-0, code, foss, java, open, source
AI DisclosureAI Assisted, Code

Download

Download NowName your own price

Click download now to get access to the following files:

JinPad.java 49 kB

Development log

Leave a comment

Log in with itch.io to leave a comment.