/*******************************************************************************
* Breakout Cave Survey Visualizer
*
* Copyright (C) 2014 James Edwards
*
* jedwards8 at fastmail dot fm
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*******************************************************************************/
package org.andork.ui.debug;
import java.awt.AWTEvent;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Paint;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.AWTEventListener;
import java.awt.event.ContainerEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.border.Border;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
/**
* A debug tool that figures out what component is under the mouse when you hold
* Alt and displays its component hierarchy (path to root ancestor) in a list.
* The selected component in the list is highlighted by setting its border.<br>
* <br>
*
* SwingInspector works by listening to all AWT Mouse events with an
* {@link AWTEventListener}.
*
* @author james.a.edwards
*/
public class SwingInspector extends JFrame {
private class EventHandler implements AWTEventListener {
@Override
public void eventDispatched(AWTEvent event) {
if (event instanceof MouseEvent) {
MouseEvent me = (MouseEvent) event;
if ((me.getModifiersEx() & InputEvent.ALT_DOWN_MASK) != 0) {
Component deepest = SwingUtilities.getDeepestComponentAt(
me.getComponent(), me.getX(), me.getY());
setDisplayedComponent(deepest);
}
} else if (event instanceof ContainerEvent) {
ContainerEvent ce = (ContainerEvent) event;
if (ce.getID() == ContainerEvent.COMPONENT_ADDED) {
stackTraces.put(ce.getChild(), new RuntimeException().getStackTrace());
} else if (ce.getID() == ContainerEvent.COMPONENT_REMOVED) {
stackTraces.remove(ce.getChild());
}
} else if (event instanceof KeyEvent) {
KeyEvent ke = (KeyEvent) event;
if (ke.getKeyCode() == KeyEvent.VK_D && (ke.getModifiersEx()
& (InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)) != 0) {
Window window = SwingUtilities.getWindowAncestor(ke.getComponent());
if (window instanceof JDialog) {
JDialog dialog = (JDialog) window;
dialog.setModal(false);
dialog.setVisible(false);
dialog.setVisible(true);
}
}
}
}
}
/**
* A border with a red outline and semitransparent fill to show which
* component is selected in the hierarchy list. Keeps a reference to the
* component's original border and draws it beneath the highlight,
* preserving the insets.
*
* @author james.a.edwards
*/
private class HighlightBorder implements Border {
Border inner = null;
public HighlightBorder(Border inner) {
this.inner = inner;
}
@Override
public Insets getBorderInsets(Component c) {
return inner == null ? new Insets(0, 0, 0, 0) : inner
.getBorderInsets(c);
}
@Override
public boolean isBorderOpaque() {
return inner == null ? false : inner.isBorderOpaque();
}
@Override
public void paintBorder(Component c, Graphics g, int x, int y,
int width, int height) {
Graphics2D g2 = (Graphics2D) g;
Paint prevPaint = g2.getPaint();
g2.setColor(new Color(255, 0, 0, 128));
g2.fillRect(x, y, width - 1, height - 1);
g2.setColor(Color.RED);
g2.drawRect(x, y, width - 1, height - 1);
g2.setPaint(prevPaint);
if (inner != null) {
inner.paintBorder(c, g2, x, y, width, height);
}
}
}
/**
*
*/
private static final long serialVersionUID = 2594539320189073849L;
private ComponentTree componentTree;
private JScrollPane componentTreeScroller;
private JTextArea stackTraceArea;
private JScrollPane stackTraceAreaScroller;
private Component displayedComponent;
private Component highlightedComponent;
private JTable attributesTable;
private DefaultTableModel attributesTableModel;
private JScrollPane attributesTableScroller;
private JSplitPane topSplitPane;
private JSplitPane mainSplitPane;
private EventHandler eventHandler;
private boolean listening = false;
private Map<Component, StackTraceElement[]> stackTraces = new HashMap<Component, StackTraceElement[]>();
public SwingInspector() {
super("Swing Inspector (Ctrl + Alt + Shift + S)");
init();
}
private int getAttrEnd(String attrStr, Matcher m, int start) {
int level = 0;
int i = start;
do {
int nextOpen = attrStr.indexOf('[', i);
int nextClose = attrStr.indexOf(']', i);
int nextAttr = -1;
if (m.find(i)) {
nextAttr = m.start();
}
if (nextOpen < 0) {
nextOpen = attrStr.length();
}
if (nextClose < 0) {
nextClose = attrStr.length();
}
if (nextAttr < 0 || level > 0) {
nextAttr = attrStr.length();
}
if (nextOpen < nextClose && nextOpen < nextAttr) {
level++;
i = nextOpen + 1;
} else if (nextClose < nextOpen && nextClose < nextAttr) {
level--;
i = nextClose + 1;
} else {
level = 0;
i = nextAttr;
}
} while (level > 0);
return Math.min(attrStr.length() - 1, i);
}
private void init() {
componentTree = new ComponentTree();
componentTree.setAWTEventHandlerRegistered(true);
componentTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
componentTreeScroller = new JScrollPane(componentTree);
componentTree.addTreeSelectionListener(new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent e) {
TreePath newPath = e.getNewLeadSelectionPath();
if (newPath == null) {
setHighlightedComponent(null);
} else {
DefaultMutableTreeNode lastNode = (DefaultMutableTreeNode) newPath.getLastPathComponent();
setHighlightedComponent((Component) lastNode.getUserObject());
}
}
});
eventHandler = new EventHandler();
Toolkit toolkit = Toolkit.getDefaultToolkit();
toolkit.addAWTEventListener(eventHandler, AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK
| AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.CONTAINER_EVENT_MASK);
attributesTableModel = new DefaultTableModel(new Object[] { "Attribute", "Value" }, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
};
attributesTable = new JTable(attributesTableModel);
attributesTableScroller = new JScrollPane(attributesTable);
attributesTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
attributesTable.getColumnModel().getColumn(0).setPreferredWidth(200);
attributesTable.getColumnModel().getColumn(1).setPreferredWidth(2000);
JPanel hierarchyPanel = new JPanel(new BorderLayout());
JLabel hierarchyLabel = new JLabel("Holt Alt and drag mouse over components to see component hierarchy!");
hierarchyLabel.setFont(hierarchyLabel.getFont().deriveFont(Font.BOLD));
hierarchyPanel.add(hierarchyLabel, BorderLayout.NORTH);
// hierarchyPanel.add(hierarchyListScroller, BorderLayout.CENTER);
hierarchyPanel.add(componentTreeScroller, BorderLayout.CENTER);
topSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
topSplitPane.setLeftComponent(hierarchyPanel);
topSplitPane.setRightComponent(attributesTableScroller);
topSplitPane.setResizeWeight(0.5);
stackTraceArea = new JTextArea();
stackTraceArea.setEditable(false);
stackTraceArea.setForeground(Color.RED);
stackTraceArea.setFont(new Font("Monospaced", Font.PLAIN, 11));
stackTraceAreaScroller = new JScrollPane(stackTraceArea);
JPanel stackTracePanel = new JPanel(new BorderLayout());
JLabel stackTraceLabel = new JLabel("Stack trace where component was added:");
stackTraceLabel.setFont(stackTraceLabel.getFont().deriveFont(Font.BOLD));
stackTracePanel.add(stackTraceLabel, BorderLayout.NORTH);
stackTracePanel.add(stackTraceAreaScroller, BorderLayout.CENTER);
mainSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
mainSplitPane.setResizeWeight(0.5);
mainSplitPane.setTopComponent(topSplitPane);
mainSplitPane.setBottomComponent(stackTracePanel);
getContentPane().add(mainSplitPane, BorderLayout.CENTER);
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
setSize(screenSize.width * 2 / 3, screenSize.height * 2 / 3);
mainSplitPane.setDividerLocation(getHeight() / 2);
topSplitPane.setDividerLocation(getWidth() / 2);
setLocationRelativeTo(null);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
}
/**
* Sets the component whose path to root is displayed in the list, and
* selects that component (which will be last) in the list.
*
* @param comp
* the new component to show.
*/
public void setDisplayedComponent(final Component comp) {
if (displayedComponent != comp) {
displayedComponent = comp;
componentTree.focus(comp);
}
}
/**
* Sets the highlighted component. The highlight border will be removed from
* the old highlighted component, and a highlight border will be added to
* the new one.
*
* @param selectedValue
*/
private void setHighlightedComponent(Component selectedValue) {
if (selectedValue != highlightedComponent) {
componentTree.focus(selectedValue);
if (highlightedComponent != null
&& highlightedComponent instanceof JComponent) {
JComponent jsel = (JComponent) highlightedComponent;
jsel.putClientProperty("highlightedBySwingInspector", null);
if (jsel.getBorder() instanceof HighlightBorder) {
jsel.setBorder(((HighlightBorder) jsel.getBorder()).inner);
}
}
highlightedComponent = selectedValue;
if (highlightedComponent != null
&& highlightedComponent instanceof JComponent) {
JComponent jsel = (JComponent) highlightedComponent;
try {
jsel.setBorder(new HighlightBorder(jsel.getBorder()));
} catch (Exception ex) {
}
}
stackTraceArea.setText(null);
StackTraceElement[] trace = stackTraces.get(highlightedComponent);
if (trace != null) {
StringBuffer sb = new StringBuffer();
for (StackTraceElement elem : trace) {
if (sb.length() > 0) {
sb.append("\n\t");
}
sb.append(elem);
}
stackTraceArea.setText(sb.toString());
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
stackTraceAreaScroller.getVerticalScrollBar().setValue(0);
}
});
}
attributesTableModel.setRowCount(0);
if (highlightedComponent != null) {
if (highlightedComponent instanceof JComponent) {
JComponent jsel = (JComponent) highlightedComponent;
jsel.putClientProperty("highlightedBySwingInspector", true);
}
String attrStr = highlightedComponent.toString();
Pattern attrPat = Pattern.compile(",\\w[a-zA-Z0-9_]*=");
Matcher m = attrPat.matcher(attrStr);
HashMap<String, String> attrMap = new HashMap<String, String>();
int start = 0;
while (m.find(start)) {
int valueEnd = getAttrEnd(attrStr, attrPat.matcher(attrStr), m.end());
attrMap.put(attrStr.substring(m.start() + 1, m.end() - 1), attrStr.substring(m.end(), valueEnd));
start = valueEnd;
}
Pattern basicsPat = Pattern.compile("^.*\\[(.*),(\\d+),(\\d+),(\\d+)x(\\d+)");
m = basicsPat.matcher(attrStr);
if (m.find()) {
attrMap.put("name", m.group(1));
attrMap.put("x", m.group(2));
attrMap.put("y", m.group(3));
attrMap.put("width", m.group(4));
attrMap.put("height", m.group(5));
}
String[] attrs = attrMap.keySet().toArray(new String[attrMap.size()]);
Arrays.sort(attrs);
for (String attr : attrs) {
attributesTableModel.addRow(new Object[] { attr, attrMap.get(attr) });
}
}
}
}
}