/* * Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. * Use of this file is governed by the BSD 3-clause license that * can be found in the LICENSE.txt file in the project root. */ package org.antlr.v4.gui; import org.abego.treelayout.NodeExtentProvider; import org.abego.treelayout.TreeForTreeLayout; import org.abego.treelayout.TreeLayout; import org.abego.treelayout.util.DefaultConfiguration; import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.misc.Utils; import org.antlr.v4.runtime.tree.ErrorNode; import org.antlr.v4.runtime.tree.Tree; import org.antlr.v4.runtime.tree.Trees; import javax.imageio.ImageIO; import javax.print.PrintException; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.filechooser.FileFilter; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.awt.geom.CubicCurve2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.prefs.Preferences; public class TreeViewer extends JComponent { public static final Color LIGHT_RED = new Color(244, 213, 211); public static class DefaultTreeTextProvider implements TreeTextProvider { private final List<String> ruleNames; public DefaultTreeTextProvider(List<String> ruleNames) { this.ruleNames = ruleNames; } @Override public String getText(Tree node) { return String.valueOf(Trees.getNodeText(node, ruleNames)); } } public static class VariableExtentProvide implements NodeExtentProvider<Tree> { TreeViewer viewer; public VariableExtentProvide(TreeViewer viewer) { this.viewer = viewer; } @Override public double getWidth(Tree tree) { FontMetrics fontMetrics = viewer.getFontMetrics(viewer.font); String s = viewer.getText(tree); int w = fontMetrics.stringWidth(s) + viewer.nodeWidthPadding*2; return w; } @Override public double getHeight(Tree tree) { FontMetrics fontMetrics = viewer.getFontMetrics(viewer.font); int h = fontMetrics.getHeight() + viewer.nodeHeightPadding*2; String s = viewer.getText(tree); String[] lines = s.split("\n"); return h * lines.length; } } protected TreeTextProvider treeTextProvider; protected TreeLayout<Tree> treeLayout; protected java.util.List<Tree> highlightedNodes; protected String fontName = "Helvetica"; //Font.SANS_SERIF; protected int fontStyle = Font.PLAIN; protected int fontSize = 11; protected Font font = new Font(fontName, fontStyle, fontSize); protected double gapBetweenLevels = 17; protected double gapBetweenNodes = 7; protected int nodeWidthPadding = 2; // added to left/right protected int nodeHeightPadding = 0; // added above/below protected int arcSize = 0; // make an arc in node outline? protected double scale = 1.0; protected Color boxColor = null; // set to a color to make it draw background protected Color highlightedBoxColor = Color.lightGray; protected Color borderColor = null; protected Color textColor = Color.black; public TreeViewer(List<String> ruleNames, Tree tree) { setRuleNames(ruleNames); if ( tree!=null ) { setTree(tree); } setFont(font); } private void updatePreferredSize() { setPreferredSize(getScaledTreeSize()); invalidate(); if (getParent() != null) { getParent().validate(); } repaint(); } // ---------------- PAINT ----------------------------------------------- private boolean useCurvedEdges = false; public boolean getUseCurvedEdges() { return useCurvedEdges; } public void setUseCurvedEdges(boolean useCurvedEdges) { this.useCurvedEdges = useCurvedEdges; } protected void paintEdges(Graphics g, Tree parent) { if (!getTree().isLeaf(parent)) { BasicStroke stroke = new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); ((Graphics2D)g).setStroke(stroke); Rectangle2D.Double parentBounds = getBoundsOfNode(parent); double x1 = parentBounds.getCenterX(); double y1 = parentBounds.getMaxY(); for (Tree child : getTree().getChildren(parent)) { Rectangle2D.Double childBounds = getBoundsOfNode(child); double x2 = childBounds.getCenterX(); double y2 = childBounds.getMinY(); if (getUseCurvedEdges()) { CubicCurve2D c = new CubicCurve2D.Double(); double ctrlx1 = x1; double ctrly1 = (y1+y2)/2; double ctrlx2 = x2; double ctrly2 = y1; c.setCurve(x1, y1, ctrlx1, ctrly1, ctrlx2, ctrly2, x2, y2); ((Graphics2D) g).draw(c); } else { g.drawLine((int) x1, (int) y1, (int) x2, (int) y2); } paintEdges(g, child); } } } protected void paintBox(Graphics g, Tree tree) { Rectangle2D.Double box = getBoundsOfNode(tree); // draw the box in the background boolean ruleFailedAndMatchedNothing = false; if ( tree instanceof ParserRuleContext ) { ParserRuleContext ctx = (ParserRuleContext) tree; ruleFailedAndMatchedNothing = ctx.exception != null && ctx.stop != null && ctx.stop.getTokenIndex() < ctx.start.getTokenIndex(); } if ( isHighlighted(tree) || boxColor!=null || tree instanceof ErrorNode || ruleFailedAndMatchedNothing) { if ( isHighlighted(tree) ) g.setColor(highlightedBoxColor); else if ( tree instanceof ErrorNode || ruleFailedAndMatchedNothing ) g.setColor(LIGHT_RED); else g.setColor(boxColor); g.fillRoundRect((int) box.x, (int) box.y, (int) box.width - 1, (int) box.height - 1, arcSize, arcSize); } if ( borderColor!=null ) { g.setColor(borderColor); g.drawRoundRect((int) box.x, (int) box.y, (int) box.width - 1, (int) box.height - 1, arcSize, arcSize); } // draw the text on top of the box (possibly multiple lines) g.setColor(textColor); String s = getText(tree); String[] lines = s.split("\n"); FontMetrics m = getFontMetrics(font); int x = (int) box.x + arcSize / 2 + nodeWidthPadding; int y = (int) box.y + m.getAscent() + m.getLeading() + 1 + nodeHeightPadding; for (int i = 0; i < lines.length; i++) { text(g, lines[i], x, y); y += m.getHeight(); } } public void text(Graphics g, String s, int x, int y) { // System.out.println("drawing '"+s+"' @ "+x+","+y); s = Utils.escapeWhitespace(s, true); g.drawString(s, x, y); } @Override public void paint(Graphics g) { super.paint(g); if ( treeLayout==null ) { return; } Graphics2D g2 = (Graphics2D)g; // anti-alias the lines g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Anti-alias the text g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); // AffineTransform at = g2.getTransform(); // g2.scale( // (double) this.getWidth() / 400, // (double) this.getHeight() / 400); // // g2.setTransform(at); paintEdges(g, getTree().getRoot()); // paint the boxes for (Tree Tree : treeLayout.getNodeBounds().keySet()) { paintBox(g, Tree); } } @Override protected Graphics getComponentGraphics(Graphics g) { Graphics2D g2d=(Graphics2D)g; g2d.scale(scale, scale); return super.getComponentGraphics(g2d); } // ---------------------------------------------------------------------- private static final String DIALOG_WIDTH_PREFS_KEY = "dialog_width"; private static final String DIALOG_HEIGHT_PREFS_KEY = "dialog_height"; private static final String DIALOG_X_PREFS_KEY = "dialog_x"; private static final String DIALOG_Y_PREFS_KEY = "dialog_y"; private static final String DIALOG_DIVIDER_LOC_PREFS_KEY = "dialog_divider_location"; private static final String DIALOG_VIEWER_SCALE_PREFS_KEY = "dialog_viewer_scale"; protected static JFrame showInDialog(final TreeViewer viewer) { final JFrame dialog = new JFrame(); dialog.setTitle("Parse Tree Inspector"); final Preferences prefs = Preferences.userNodeForPackage(TreeViewer.class); // Make new content panes final Container mainPane = new JPanel(new BorderLayout(5,5)); final Container contentPane = new JPanel(new BorderLayout(0,0)); contentPane.setBackground(Color.white); // Wrap viewer in scroll pane JScrollPane scrollPane = new JScrollPane(viewer); // Make the scrollpane (containing the viewer) the center component contentPane.add(scrollPane, BorderLayout.CENTER); JPanel wrapper = new JPanel(new FlowLayout()); // Add button to bottom JPanel bottomPanel = new JPanel(new BorderLayout(0,0)); contentPane.add(bottomPanel, BorderLayout.SOUTH); JButton ok = new JButton("OK"); ok.addActionListener( new ActionListener() { @Override public void actionPerformed(ActionEvent e) { dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING)); } } ); wrapper.add(ok); // Add an export-to-png button right of the "OK" button JButton png = new JButton("Export as PNG"); png.addActionListener( new ActionListener() { @Override public void actionPerformed(ActionEvent e) { generatePNGFile(viewer, dialog); } } ); wrapper.add(png); bottomPanel.add(wrapper, BorderLayout.SOUTH); // Add scale slider double lastKnownViewerScale = prefs.getDouble(DIALOG_VIEWER_SCALE_PREFS_KEY, viewer.getScale()); viewer.setScale(lastKnownViewerScale); int sliderValue = (int) ((lastKnownViewerScale - 1.0) * 1000); final JSlider scaleSlider = new JSlider(JSlider.HORIZONTAL, -999, 1000, sliderValue); scaleSlider.addChangeListener( new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { int v = scaleSlider.getValue(); viewer.setScale(v / 1000.0 + 1.0); } } ); bottomPanel.add(scaleSlider, BorderLayout.CENTER); // Add a JTree representing the parser tree of the input. JPanel treePanel = new JPanel(new BorderLayout(5, 5)); // An "empty" icon that will be used for the JTree's nodes. Icon empty = new EmptyIcon(); UIManager.put("Tree.closedIcon", empty); UIManager.put("Tree.openIcon", empty); UIManager.put("Tree.leafIcon", empty); Tree parseTreeRoot = viewer.getTree().getRoot(); TreeNodeWrapper nodeRoot = new TreeNodeWrapper(parseTreeRoot, viewer); fillTree(nodeRoot, parseTreeRoot, viewer); final JTree tree = new JTree(nodeRoot); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); tree.addTreeSelectionListener(new TreeSelectionListener() { @Override public void valueChanged(TreeSelectionEvent e) { JTree selectedTree = (JTree) e.getSource(); TreePath path = selectedTree.getSelectionPath(); if (path!=null) { TreeNodeWrapper treeNode = (TreeNodeWrapper) path.getLastPathComponent(); // Set the clicked AST. viewer.setTree((Tree) treeNode.getUserObject()); } } }); treePanel.add(new JScrollPane(tree)); // Create the pane for both the JTree and the AST final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, treePanel, contentPane); mainPane.add(splitPane, BorderLayout.CENTER); dialog.setContentPane(mainPane); // make viz WindowListener exitListener = new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { prefs.putInt(DIALOG_WIDTH_PREFS_KEY, (int) dialog.getSize().getWidth()); prefs.putInt(DIALOG_HEIGHT_PREFS_KEY, (int) dialog.getSize().getHeight()); prefs.putDouble(DIALOG_X_PREFS_KEY, dialog.getLocationOnScreen().getX()); prefs.putDouble(DIALOG_Y_PREFS_KEY, dialog.getLocationOnScreen().getY()); prefs.putInt(DIALOG_DIVIDER_LOC_PREFS_KEY, splitPane.getDividerLocation()); prefs.putDouble(DIALOG_VIEWER_SCALE_PREFS_KEY, viewer.getScale()); dialog.setVisible(false); dialog.dispose(); } }; dialog.addWindowListener(exitListener); dialog.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); int width = prefs.getInt(DIALOG_WIDTH_PREFS_KEY, 600); int height = prefs.getInt(DIALOG_HEIGHT_PREFS_KEY, 500); dialog.setPreferredSize(new Dimension(width, height)); dialog.pack(); // After pack(): set the divider at 1/3 (200/600) of the frame. int dividerLocation = prefs.getInt(DIALOG_DIVIDER_LOC_PREFS_KEY, 200); splitPane.setDividerLocation(dividerLocation); if (prefs.getDouble(DIALOG_X_PREFS_KEY, -1) != -1) { dialog.setLocation( (int)prefs.getDouble(DIALOG_X_PREFS_KEY, 100), (int)prefs.getDouble(DIALOG_Y_PREFS_KEY, 100) ); } else { dialog.setLocationRelativeTo(null); } dialog.setVisible(true); return dialog; } private static void generatePNGFile(TreeViewer viewer, JFrame dialog) { BufferedImage bi = new BufferedImage(viewer.getSize().width, viewer.getSize().height, BufferedImage.TYPE_INT_ARGB); Graphics g = bi.createGraphics(); viewer.paint(g); g.dispose(); try { File suggestedFile = generateNonExistingPngFile(); JFileChooser fileChooser = new JFileChooserConfirmOverwrite(); fileChooser.setCurrentDirectory(suggestedFile.getParentFile()); fileChooser.setSelectedFile(suggestedFile); FileFilter pngFilter = new FileFilter() { @Override public boolean accept(File pathname) { if (pathname.isFile()) { return pathname.getName().toLowerCase().endsWith(".png"); } return true; } @Override public String getDescription() { return "PNG Files (*.png)"; } }; fileChooser.addChoosableFileFilter(pngFilter); fileChooser.setFileFilter(pngFilter); int returnValue = fileChooser.showSaveDialog(dialog); if (returnValue == JFileChooser.APPROVE_OPTION) { File pngFile = fileChooser.getSelectedFile(); ImageIO.write(bi, "png", pngFile); try { // Try to open the parent folder using the OS' native file manager. Desktop.getDesktop().open(pngFile.getParentFile()); } catch (Exception ex) { // We could not launch the file manager: just show a popup that we // succeeded in saving the PNG file. JOptionPane.showMessageDialog(dialog, "Saved PNG to: " + pngFile.getAbsolutePath()); ex.printStackTrace(); } } } catch (Exception ex) { JOptionPane.showMessageDialog(dialog, "Could not export to PNG: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); ex.printStackTrace(); } } private static File generateNonExistingPngFile() { final String parent = "."; final String name = "antlr4_parse_tree"; final String extension = ".png"; File pngFile = new File(parent, name + extension); int counter = 1; // Keep looping until we create a File that does not yet exist. while (pngFile.exists()) { pngFile = new File(parent, name + "_"+ counter + extension); counter++; } return pngFile; } private static void fillTree(TreeNodeWrapper node, Tree tree, TreeViewer viewer) { if (tree == null) { return; } for (int i = 0; i < tree.getChildCount(); i++) { Tree childTree = tree.getChild(i); TreeNodeWrapper childNode = new TreeNodeWrapper(childTree, viewer); node.add(childNode); fillTree(childNode, childTree, viewer); } } private Dimension getScaledTreeSize() { Dimension scaledTreeSize = treeLayout.getBounds().getBounds().getSize(); scaledTreeSize = new Dimension((int)(scaledTreeSize.width*scale), (int)(scaledTreeSize.height*scale)); return scaledTreeSize; } public Future<JFrame> open() { final TreeViewer viewer = this; viewer.setScale(1.5); Callable<JFrame> callable = new Callable<JFrame>() { JFrame result; @Override public JFrame call() throws Exception { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { result = showInDialog(viewer); } }); return result; } }; ExecutorService executor = Executors.newSingleThreadExecutor(); try { return executor.submit(callable); } finally { executor.shutdown(); } } public void save(String fileName) throws IOException, PrintException { JFrame dialog = new JFrame(); Container contentPane = dialog.getContentPane(); ((JComponent) contentPane).setBorder(BorderFactory.createEmptyBorder( 10, 10, 10, 10)); contentPane.add(this); contentPane.setBackground(Color.white); dialog.pack(); dialog.setLocationRelativeTo(null); dialog.dispose(); GraphicsSupport.saveImage(this, fileName); } // --------------------------------------------------- protected Rectangle2D.Double getBoundsOfNode(Tree node) { return treeLayout.getNodeBounds().get(node); } protected String getText(Tree tree) { String s = treeTextProvider.getText(tree); s = Utils.escapeWhitespace(s, true); return s; } public TreeTextProvider getTreeTextProvider() { return treeTextProvider; } public void setTreeTextProvider(TreeTextProvider treeTextProvider) { this.treeTextProvider = treeTextProvider; } public void setFontSize(int sz) { fontSize = sz; font = new Font(fontName, fontStyle, fontSize); } public void setFontName(String name) { fontName = name; font = new Font(fontName, fontStyle, fontSize); } /** Slow for big lists of highlighted nodes */ public void addHighlightedNodes(Collection<Tree> nodes) { highlightedNodes = new ArrayList<Tree>(); highlightedNodes.addAll(nodes); } public void removeHighlightedNodes(Collection<Tree> nodes) { if ( highlightedNodes!=null ) { // only remove exact objects defined by ==, not equals() for (Tree t : nodes) { int i = getHighlightedNodeIndex(t); if ( i>=0 ) highlightedNodes.remove(i); } } } protected boolean isHighlighted(Tree node) { return getHighlightedNodeIndex(node) >= 0; } protected int getHighlightedNodeIndex(Tree node) { if ( highlightedNodes==null ) return -1; for (int i = 0; i < highlightedNodes.size(); i++) { Tree t = highlightedNodes.get(i); if ( t == node ) return i; } return -1; } @Override public Font getFont() { return font; } @Override public void setFont(Font font) { this.font = font; } public int getArcSize() { return arcSize; } public void setArcSize(int arcSize) { this.arcSize = arcSize; } public Color getBoxColor() { return boxColor; } public void setBoxColor(Color boxColor) { this.boxColor = boxColor; } public Color getHighlightedBoxColor() { return highlightedBoxColor; } public void setHighlightedBoxColor(Color highlightedBoxColor) { this.highlightedBoxColor = highlightedBoxColor; } public Color getBorderColor() { return borderColor; } public void setBorderColor(Color borderColor) { this.borderColor = borderColor; } public Color getTextColor() { return textColor; } public void setTextColor(Color textColor) { this.textColor = textColor; } protected TreeForTreeLayout<Tree> getTree() { return treeLayout.getTree(); } public void setTree(Tree root) { if ( root!=null ) { boolean useIdentity = true; // compare node identity this.treeLayout = new TreeLayout<Tree>(getTreeLayoutAdaptor(root), new TreeViewer.VariableExtentProvide(this), new DefaultConfiguration<Tree>(gapBetweenLevels, gapBetweenNodes), useIdentity); // Let the UI display this new AST. updatePreferredSize(); } else { this.treeLayout = null; repaint(); } } /** Get an adaptor for root that indicates how to walk ANTLR trees. * Override to change the adapter from the default of {@link TreeLayoutAdaptor} */ public TreeForTreeLayout<Tree> getTreeLayoutAdaptor(Tree root) { return new TreeLayoutAdaptor(root); } public double getScale() { return scale; } public void setScale(double scale) { if(scale <= 0) { scale = 1; } this.scale = scale; updatePreferredSize(); } public void setRuleNames(List<String> ruleNames) { setTreeTextProvider(new DefaultTreeTextProvider(ruleNames)); } private static class TreeNodeWrapper extends DefaultMutableTreeNode { final TreeViewer viewer; TreeNodeWrapper(Tree tree, TreeViewer viewer) { super(tree); this.viewer = viewer; } @Override public String toString() { return viewer.getText((Tree) this.getUserObject()); } } private static class EmptyIcon implements Icon { @Override public int getIconWidth() { return 0; } @Override public int getIconHeight() { return 0; } @Override public void paintIcon(Component c, Graphics g, int x, int y) { /* Do nothing. */ } } }