// Copyright (c) 2003-present, Jodd Team (http://jodd.org) // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. package jodd.swingspy; import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JEditorPane; import javax.swing.JFormattedTextField; import javax.swing.JLabel; import javax.swing.JLayeredPane; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JPasswordField; import javax.swing.JProgressBar; import javax.swing.JRadioButton; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JSeparator; import javax.swing.JSlider; import javax.swing.JSpinner; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.JTable; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.JTextPane; import javax.swing.JToolBar; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.TreePath; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Font; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.net.URL; import java.util.TreeSet; /** * SwingSpy GUI. */ public class SwingSpyPanel extends JPanel { private final JTree componentTree; private final JEditorPane componentData; private final JEditorPane detailsData; private final JScrollPane detailsScrollPane; private final DefaultMutableTreeNode root; private static final int INITIAL_WIDTH = 600; private static final int INITIAL_HEIGHT = 500; private final Font font = new Font("Arial", Font.PLAIN, 12); /** * Initialization. */ public SwingSpyPanel() { setPreferredSize(new Dimension(INITIAL_WIDTH, INITIAL_HEIGHT)); setLayout(new BorderLayout()); root = new DefaultMutableTreeNode(); componentTree = new JTree(root); componentTree.setRootVisible(false); componentTree.setCellRenderer(new SwingComponentRenderer()); componentTree.addTreeSelectionListener(new CustomSelectionListener()); // add(new JScrollPane(componentTree), BorderLayout.CENTER); detailsData = new JEditorPane(); detailsData.setBackground(new Color(250, 250, 250)); detailsData.setForeground(new Color(33, 33, 33)); detailsData.setBorder(BorderFactory.createLineBorder(new Color(100, 100, 244), 1)); detailsData.setPreferredSize(new Dimension(150, INITIAL_HEIGHT)); detailsData.setEditable(false); detailsData.setContentType("text/html"); SwingUtil.enforceJEditorPaneFont(detailsData, font); detailsScrollPane = new JScrollPane(detailsData); // add(detailsScrollPane, BorderLayout.EAST); JSplitPane hPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, new JScrollPane(componentTree), detailsScrollPane); hPane.setContinuousLayout(true); hPane.setOneTouchExpandable(true); hPane.setDividerLocation(INITIAL_WIDTH - 200); add(hPane, BorderLayout.CENTER); componentData = new JEditorPane(); componentData.setBackground(new Color(250, 250, 250)); componentData.setForeground(new Color(33, 33, 33)); componentData.setBorder(BorderFactory.createLineBorder(new Color(100, 100, 244), 1)); componentData.setPreferredSize(new Dimension(INITIAL_WIDTH, 36)); componentData.setEditable(false); componentData.setContentType("text/html"); SwingUtil.enforceJEditorPaneFont(componentData, font); add(componentData, BorderLayout.SOUTH); } /** * Reloads component tree completely. */ public void reload(Component rootComponent, Component selectedComponent) { root.removeAllChildren(); addNode(root, rootComponent, selectedComponent); SwingUtilities.updateComponentTreeUI(componentTree); } /** * Recursively adds new nodes to the tree. */ protected void addNode(DefaultMutableTreeNode parent, Component component, Component selectedComponent) { DefaultMutableTreeNode componentNode = new DefaultMutableTreeNode(new ComponentWrapper(component)); parent.add(componentNode); if (component == selectedComponent) { TreePath selectedPath = new TreePath(componentNode.getPath()); componentTree.setSelectionPath(selectedPath); componentTree.scrollPathToVisible(selectedPath); } if (component instanceof Container) { Container container = (Container) component; Component[] childComponents = container.getComponents(); for (Component child : childComponents) { addNode(componentNode, child, selectedComponent); } } } // ---------------------------------------------------------------- component wrapper /** * Simple component wrapper that has nice <code>toString</code> representation, * suitable for the tree view. */ static class ComponentWrapper { Component component; ComponentWrapper(Component component) { this.component = component; } @Override public String toString() { String name = component.getName(); return (name != null ? name + " " : "") + '(' + component.getClass().getSimpleName() + ')'; } public String toHtmlString() { return new StringBuilder("<html>").append(" name: ") .append("<b>").append(component.getName()) .append("</b><br>").append(" class: ").append("<b>") .append(component.getClass().getName()).append("</b><br>") .toString(); } public String toDetailedString() { Object bean = component; if (bean == null) { return "<null>"; } TreeSet<String> treeSet = new TreeSet<>(); Class clazz = bean.getClass(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { StringBuilder str = new StringBuilder(); int modifiers = field.getModifiers(); if (Modifier.isPublic(modifiers)) { str.append('+'); } else if (Modifier.isProtected(modifiers)) { str.append('#'); } else if (Modifier.isPrivate(modifiers)) { str.append('-'); } else { str.append(' '); } field.setAccessible(true); str.append(field.getName()).append(':'); try { Object value = field.get(bean); if (value == null) { str.append("<null>"); } else { String valueString = value.toString(); if (valueString.length() > 250) { valueString = valueString.substring(0, 250) + "..."; } str.append(valueString); } } catch (IllegalAccessException ignore) { str.append("N/A"); } str.append('\n'); treeSet.add(htmlSafe(str.toString())); } StringBuilder resolve = new StringBuilder("<html>"); for (String s1 : treeSet) { resolve.append(s1); } return resolve.toString(); } } private static String htmlSafe(String str) { str = str.replace(">", ">"); str = str.replace("<", "<"); str = str.replace(" ", " "); str = str.replace("\n", "<br>"); return str; } // ---------------------------------------------------------------- selection listener /** * Selection listener. */ class CustomSelectionListener implements TreeSelectionListener { public void valueChanged(TreeSelectionEvent tse) { DefaultMutableTreeNode lastNode = (DefaultMutableTreeNode) tse.getPath().getLastPathComponent(); ComponentWrapper cmp = (ComponentWrapper) lastNode.getUserObject(); componentData.setText(cmp.toHtmlString()); detailsData.setText(cmp.toDetailedString()); SwingUtil.scrollToTopLater(detailsScrollPane); } } // ---------------------------------------------------------------- custom renderer /** * Displays node icons based on component type. */ static class SwingComponentRenderer extends DefaultTreeCellRenderer { private static ImageIcon noneIcon; private static Class[] cmpClasses; private static ImageIcon[] cmpIcons; static { String packageName = SwingComponentRenderer.class.getPackage().getName(); packageName = '/' + packageName.replace('.', '/') + "/icons/"; noneIcon = createImageIcon(packageName + "none.png"); cmpClasses = new Class[]{ JButton.class, JCheckBox.class, JComboBox.class, JLabel.class, JList.class, JPanel.class, JProgressBar.class, JRadioButton.class, JScrollBar.class, JScrollPane.class, JSlider.class, JTabbedPane.class, JTextArea.class, JTextField.class, JTree.class, JEditorPane.class, JFormattedTextField.class, JPasswordField.class, JSpinner.class, JTable.class, JTextPane.class, JToolBar.class, JLayeredPane.class, JSplitPane.class, JSeparator.class, }; cmpIcons = new ImageIcon[]{ createImageIcon(packageName + "button.png"), createImageIcon(packageName + "checkBox.png"), createImageIcon(packageName + "comboBox.png"), createImageIcon(packageName + "label.png"), createImageIcon(packageName + "list.png"), createImageIcon(packageName + "panel.png"), createImageIcon(packageName + "progressbar.png"), createImageIcon(packageName + "radioButton.png"), createImageIcon(packageName + "scrollbar.png"), createImageIcon(packageName + "scrollPane.png"), createImageIcon(packageName + "slider.png"), createImageIcon(packageName + "tabbedPane.png"), createImageIcon(packageName + "textArea.png"), createImageIcon(packageName + "textField.png"), createImageIcon(packageName + "tree.png"), createImageIcon(packageName + "editorPane.png"), createImageIcon(packageName + "formattedTextField.png"), createImageIcon(packageName + "passwordField.png"), createImageIcon(packageName + "spinner.png"), createImageIcon(packageName + "table.png"), createImageIcon(packageName + "textPane.png"), createImageIcon(packageName + "toolbar.png"), createImageIcon(packageName + "panel.png"), createImageIcon(packageName + "splitPane.png"), createImageIcon(packageName + "separator.png"), }; } protected static ImageIcon createImageIcon(String path) { URL imgURL = SwingComponentRenderer.class.getResource(path); if (imgURL != null) { return new ImageIcon(imgURL); } else { System.err.println("Couldn't find icon file: " + path); return null; } } @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; if (node.getUserObject() != null) { ComponentWrapper userObject = (ComponentWrapper) node.getUserObject(); if (userObject != null) { Component c = userObject.component; for (int i = 0; i < cmpClasses.length; i++) { Class clazz = cmpClasses[i]; if (clazz.isAssignableFrom(c.getClass())) { setIcon(cmpIcons[i]); return this; } } } setIcon(noneIcon); } return this; } } }