All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.antlr.v4.gui.TreeViewer Maven / Gradle / Ivy

There is a newer version: 4.9.0
Show newest version
/*
 * Copyright (c) 2012 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.NotNull;
import org.antlr.v4.runtime.misc.Nullable;
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.BorderFactory;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JSplitPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
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.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
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;

@SuppressWarnings("serial")
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 ruleNames;

		public DefaultTreeTextProvider(@Nullable List ruleNames) {
			this.ruleNames = ruleNames;
		}

		@Override
		public String getText(Tree node) {
			return String.valueOf(Trees.getNodeText(node, ruleNames));
		}
	}

	public static class VariableExtentProvide implements NodeExtentProvider {
		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 treeLayout;
	protected java.util.List 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(@Nullable List 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";

	@NotNull
	protected static JDialog showInDialog(final TreeViewer viewer) {
		final JDialog dialog = new JDialog();
		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, JDialog 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;
	}

	@NotNull
	public Future open() {
		final TreeViewer viewer = this;
		viewer.setScale(1.5);
		Callable callable = new Callable() {
			JDialog result;

			@Override
			public JDialog 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 {
		JDialog dialog = new JDialog();
		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 nodes) {
		highlightedNodes = new ArrayList();
		highlightedNodes.addAll(nodes);
	}

	public void removeHighlightedNodes(Collection 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 getTree() {
		return treeLayout.getTree();
	}

	public void setTree(Tree root) {
		if ( root!=null ) {
			boolean useIdentity = true; // compare node identity
			this.treeLayout =
				new TreeLayout(getTreeLayoutAdaptor(root),
									 new TreeViewer.VariableExtentProvide(this),
									 new DefaultConfiguration(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 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 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. */
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy