blob: bfa15b3d4608ed5c33c1e67d9db9b65531baa289 [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.hierarchyviewer.ui;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.IDevice;
import com.android.hierarchyviewer.device.DeviceBridge;
import com.android.hierarchyviewer.device.Window;
import com.android.hierarchyviewer.laf.UnifiedContentBorder;
import com.android.hierarchyviewer.scene.CaptureLoader;
import com.android.hierarchyviewer.scene.ProfilesLoader;
import com.android.hierarchyviewer.scene.VersionLoader;
import com.android.hierarchyviewer.scene.ViewHierarchyLoader;
import com.android.hierarchyviewer.scene.ViewHierarchyScene;
import com.android.hierarchyviewer.scene.ViewManager;
import com.android.hierarchyviewer.scene.ViewNode;
import com.android.hierarchyviewer.scene.WindowsLoader;
import com.android.hierarchyviewer.ui.action.CaptureLayersAction;
import com.android.hierarchyviewer.ui.action.CaptureNodeAction;
import com.android.hierarchyviewer.ui.action.DumpDisplayListAction;
import com.android.hierarchyviewer.ui.action.ExitAction;
import com.android.hierarchyviewer.ui.action.InvalidateAction;
import com.android.hierarchyviewer.ui.action.LoadGraphAction;
import com.android.hierarchyviewer.ui.action.RefreshWindowsAction;
import com.android.hierarchyviewer.ui.action.RequestLayoutAction;
import com.android.hierarchyviewer.ui.action.SaveSceneAction;
import com.android.hierarchyviewer.ui.action.ShowDevicesAction;
import com.android.hierarchyviewer.ui.action.StartServerAction;
import com.android.hierarchyviewer.ui.action.StopServerAction;
import com.android.hierarchyviewer.ui.model.ProfilesTableModel;
import com.android.hierarchyviewer.ui.model.PropertiesTableModel;
import com.android.hierarchyviewer.ui.model.ViewsTreeModel;
import com.android.hierarchyviewer.ui.util.IconLoader;
import com.android.hierarchyviewer.ui.util.PngFileFilter;
import com.android.hierarchyviewer.ui.util.PsdFileFilter;
import com.android.hierarchyviewer.util.OS;
import com.android.hierarchyviewer.util.WorkerThread;
import org.netbeans.api.visual.graph.layout.TreeGraphLayout;
import org.netbeans.api.visual.model.ObjectSceneEvent;
import org.netbeans.api.visual.model.ObjectSceneEventType;
import org.netbeans.api.visual.model.ObjectSceneListener;
import org.netbeans.api.visual.model.ObjectState;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.imageio.ImageIO;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.ButtonGroup;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreePath;
public class Workspace extends JFrame {
private JLabel viewCountLabel;
private JSlider zoomSlider;
private JSplitPane sideSplitter;
private JSplitPane mainSplitter;
private JTable propertiesTable;
private JTable profilingTable;
private JComponent pixelPerfectPanel;
private JTree pixelPerfectTree;
private ScreenViewer screenViewer;
private JPanel extrasPanel;
private LayoutRenderer layoutView;
private JScrollPane sceneScroller;
private JComponent sceneView;
private ViewHierarchyScene scene;
private ActionMap actionsMap;
private JPanel mainPanel;
private JProgressBar progress;
private JToolBar buttonsPanel;
private JToolBar commandButtonsPanel;
private JComponent deviceSelector;
private DevicesTableModel devicesTableModel;
private WindowsTableModel windowsTableModel;
private IDevice currentDevice;
private Window currentWindow = Window.FOCUSED_WINDOW;
private JButton displayNodeButton;
private JButton dumpDisplayListButton;
private JButton captureLayersButton;
private JButton invalidateButton;
private JButton requestLayoutButton;
private JButton loadButton;
private JButton startButton;
private JButton stopButton;
private JButton showDevicesButton;
private JButton refreshButton;
private JToggleButton graphViewButton;
private JToggleButton pixelPerfectViewButton;
private JMenuItem saveMenuItem;
private JMenuItem showDevicesMenuItem;
private JMenuItem loadMenuItem;
private JMenuItem startMenuItem;
private JMenuItem stopMenuItem;
private JTable devices;
private JTable windows;
private JLabel minZoomLabel;
private JLabel maxZoomLabel;
private JTextField filterText;
private JLabel filterLabel;
private int protocolVersion;
private int serverVersion;
public Workspace() {
super("Hierarchy Viewer");
buildActions();
add(buildMainPanel());
setJMenuBar(buildMenuBar());
devices.changeSelection(0, 0, false, false);
currentDeviceChanged();
pack();
}
private void buildActions() {
actionsMap = new ActionMap();
actionsMap.put(ExitAction.ACTION_NAME, new ExitAction(this));
actionsMap.put(ShowDevicesAction.ACTION_NAME, new ShowDevicesAction(this));
actionsMap.put(LoadGraphAction.ACTION_NAME, new LoadGraphAction(this));
actionsMap.put(SaveSceneAction.ACTION_NAME, new SaveSceneAction(this));
actionsMap.put(StartServerAction.ACTION_NAME, new StartServerAction(this));
actionsMap.put(StopServerAction.ACTION_NAME, new StopServerAction(this));
actionsMap.put(InvalidateAction.ACTION_NAME, new InvalidateAction(this));
actionsMap.put(RequestLayoutAction.ACTION_NAME, new RequestLayoutAction(this));
actionsMap.put(DumpDisplayListAction.ACTION_NAME, new DumpDisplayListAction(this));
actionsMap.put(CaptureNodeAction.ACTION_NAME, new CaptureNodeAction(this));
actionsMap.put(CaptureLayersAction.ACTION_NAME, new CaptureLayersAction(this));
actionsMap.put(RefreshWindowsAction.ACTION_NAME, new RefreshWindowsAction(this));
}
private JComponent buildMainPanel() {
mainPanel = new JPanel();
mainPanel.setLayout(new BorderLayout());
commandButtonsPanel = buildToolBar();
mainPanel.add(commandButtonsPanel, BorderLayout.PAGE_START);
mainPanel.add(deviceSelector = buildDeviceSelector(), BorderLayout.CENTER);
mainPanel.add(buildStatusPanel(), BorderLayout.SOUTH);
mainPanel.setPreferredSize(new Dimension(1200, 800));
return mainPanel;
}
private JComponent buildGraphPanel() {
sceneScroller = new JScrollPane();
sceneScroller.setBorder(null);
mainSplitter = new JSplitPane();
mainSplitter.setResizeWeight(1.0);
mainSplitter.setContinuousLayout(true);
if (OS.isMacOsX() && OS.isLeopardOrLater()) {
mainSplitter.setBorder(new UnifiedContentBorder());
}
mainSplitter.setLeftComponent(sceneScroller);
mainSplitter.setRightComponent(buildSideSplitter());
return mainSplitter;
}
private JComponent buildDeviceSelector() {
JPanel panel = new JPanel(new GridBagLayout());
if (OS.isMacOsX() && OS.isLeopardOrLater()) {
panel.setBorder(new UnifiedContentBorder());
}
devicesTableModel = new DevicesTableModel();
for (IDevice device : DeviceBridge.getDevices()) {
DeviceBridge.setupDeviceForward(device);
devicesTableModel.addDevice(device);
}
DeviceBridge.startListenForDevices(devicesTableModel);
devices = new JTable(devicesTableModel);
devices.getSelectionModel().addListSelectionListener(new DeviceSelectedListener());
devices.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
devices.setBorder(null);
JScrollPane devicesScroller = new JScrollPane(devices);
devicesScroller.setBorder(null);
panel.add(devicesScroller, new GridBagConstraints(0, 0, 1, 1, 0.5, 1.0,
GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0),
0, 0));
windowsTableModel = new WindowsTableModel();
windowsTableModel.setVisible(false);
windows = new JTable(windowsTableModel);
windows.getSelectionModel().addListSelectionListener(new WindowSelectedListener());
windows.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
windows.setBorder(null);
JScrollPane windowsScroller = new JScrollPane(windows);
windowsScroller.setBorder(null);
panel.add(windowsScroller, new GridBagConstraints(2, 0, 1, 1, 0.5, 1.0,
GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0),
0, 0));
return panel;
}
private JComponent buildSideSplitter() {
propertiesTable = new JTable();
propertiesTable.setModel(new DefaultTableModel(new Object[][] { },
new String[] { "Property", "Value" }));
propertiesTable.setBorder(null);
propertiesTable.getTableHeader().setBorder(null);
JScrollPane tableScroller = new JScrollPane(propertiesTable);
tableScroller.setBorder(null);
profilingTable = new JTable();
profilingTable.setModel(new DefaultTableModel(new Object[][] {
{ " " , " " }, { " " , " " }, { " " , " " } },
new String[] { "Operation", "Duration (ms)" }));
profilingTable.setBorder(null);
profilingTable.getTableHeader().setBorder(null);
JScrollPane firstTableScroller = new JScrollPane(profilingTable);
firstTableScroller.setBorder(null);
setVisibleRowCount(profilingTable, 5);
firstTableScroller.setMinimumSize(profilingTable.getPreferredScrollableViewportSize());
JSplitPane tablesSplitter = new JSplitPane();
tablesSplitter.setBorder(null);
tablesSplitter.setOrientation(JSplitPane.VERTICAL_SPLIT);
tablesSplitter.setResizeWeight(0);
tablesSplitter.setLeftComponent(firstTableScroller);
tablesSplitter.setBottomComponent(tableScroller);
tablesSplitter.setContinuousLayout(true);
sideSplitter = new JSplitPane();
sideSplitter.setBorder(null);
sideSplitter.setOrientation(JSplitPane.VERTICAL_SPLIT);
sideSplitter.setResizeWeight(0.5);
sideSplitter.setLeftComponent(tablesSplitter);
sideSplitter.setBottomComponent(null);
sideSplitter.setContinuousLayout(true);
return sideSplitter;
}
private JPanel buildStatusPanel() {
JPanel statusPanel = new JPanel();
statusPanel.setLayout(new BorderLayout());
JPanel leftSide = new JPanel();
leftSide.setOpaque(false);
leftSide.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 5));
leftSide.add(Box.createHorizontalStrut(6));
ButtonGroup group = new ButtonGroup();
graphViewButton = new JToggleButton(IconLoader.load(getClass(),
"/images/icon-graph-view.png"));
graphViewButton.setSelectedIcon(IconLoader.load(getClass(),
"/images/icon-graph-view-selected.png"));
graphViewButton.putClientProperty("JButton.buttonType", "segmentedTextured");
graphViewButton.putClientProperty("JButton.segmentPosition", "first");
graphViewButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
toggleGraphView();
}
});
group.add(graphViewButton);
leftSide.add(graphViewButton);
pixelPerfectViewButton = new JToggleButton(IconLoader.load(getClass(),
"/images/icon-pixel-perfect-view.png"));
pixelPerfectViewButton.setSelectedIcon(IconLoader.load(getClass(),
"/images/icon-pixel-perfect-view-selected.png"));
pixelPerfectViewButton.putClientProperty("JButton.buttonType", "segmentedTextured");
pixelPerfectViewButton.putClientProperty("JButton.segmentPosition", "last");
pixelPerfectViewButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
togglePixelPerfectView();
}
});
group.add(pixelPerfectViewButton);
leftSide.add(pixelPerfectViewButton);
graphViewButton.setSelected(true);
filterText = new JTextField(20);
filterText.putClientProperty("JComponent.sizeVariant", "small");
filterText.getDocument().addDocumentListener(new DocumentListener() {
public void insertUpdate(DocumentEvent e) {
updateFilter(e);
}
public void removeUpdate(DocumentEvent e) {
updateFilter(e);
}
public void changedUpdate(DocumentEvent e) {
updateFilter(e);
}
});
filterLabel = new JLabel("Filter by class or id:");
filterLabel.putClientProperty("JComponent.sizeVariant", "small");
filterLabel.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 6));
leftSide.add(filterLabel);
leftSide.add(filterText);
minZoomLabel = new JLabel();
minZoomLabel.setText("20%");
minZoomLabel.putClientProperty("JComponent.sizeVariant", "small");
minZoomLabel.setBorder(BorderFactory.createEmptyBorder(0, 12, 0, 0));
leftSide.add(minZoomLabel);
zoomSlider = new JSlider();
zoomSlider.putClientProperty("JComponent.sizeVariant", "small");
zoomSlider.setMaximum(200);
zoomSlider.setMinimum(20);
zoomSlider.setValue(100);
zoomSlider.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent evt) {
zoomSliderStateChanged(evt);
}
});
leftSide.add(zoomSlider);
maxZoomLabel = new JLabel();
maxZoomLabel.putClientProperty("JComponent.sizeVariant", "small");
maxZoomLabel.setText("200%");
leftSide.add(maxZoomLabel);
viewCountLabel = new JLabel();
viewCountLabel.setText("0 views");
viewCountLabel.putClientProperty("JComponent.sizeVariant", "small");
viewCountLabel.setBorder(BorderFactory.createEmptyBorder(0, 12, 0, 0));
leftSide.add(viewCountLabel);
statusPanel.add(leftSide, BorderLayout.LINE_START);
JPanel rightSide = new JPanel();
rightSide.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 12));
rightSide.setLayout(new FlowLayout(FlowLayout.RIGHT));
progress = new JProgressBar();
progress.setVisible(false);
progress.setIndeterminate(true);
progress.putClientProperty("JComponent.sizeVariant", "mini");
progress.putClientProperty("JProgressBar.style", "circular");
rightSide.add(progress);
statusPanel.add(rightSide, BorderLayout.LINE_END);
hideStatusBarComponents();
return statusPanel;
}
private void hideStatusBarComponents() {
viewCountLabel.setVisible(false);
zoomSlider.setVisible(false);
minZoomLabel.setVisible(false);
maxZoomLabel.setVisible(false);
filterLabel.setVisible(false);
filterText.setVisible(false);
}
private JToolBar buildToolBar() {
JToolBar toolBar = new JToolBar();
toolBar.setFloatable(false);
toolBar.setRollover(true);
startButton = new JButton();
startButton.setAction(actionsMap.get(StartServerAction.ACTION_NAME));
startButton.putClientProperty("JButton.buttonType", "segmentedTextured");
startButton.putClientProperty("JButton.segmentPosition", "first");
toolBar.add(startButton);
stopButton = new JButton();
stopButton.setAction(actionsMap.get(StopServerAction.ACTION_NAME));
stopButton.putClientProperty("JButton.buttonType", "segmentedTextured");
stopButton.putClientProperty("JButton.segmentPosition", "middle");
toolBar.add(stopButton);
refreshButton = new JButton();
refreshButton.setAction(actionsMap.get(RefreshWindowsAction.ACTION_NAME));
refreshButton.putClientProperty("JButton.buttonType", "segmentedTextured");
refreshButton.putClientProperty("JButton.segmentPosition", "last");
toolBar.add(refreshButton);
showDevicesButton = new JButton();
showDevicesButton.setAction(actionsMap.get(ShowDevicesAction.ACTION_NAME));
showDevicesButton.putClientProperty("JButton.buttonType", "segmentedTextured");
showDevicesButton.putClientProperty("JButton.segmentPosition", "first");
toolBar.add(showDevicesButton);
showDevicesButton.setEnabled(false);
loadButton = new JButton();
loadButton.setAction(actionsMap.get(LoadGraphAction.ACTION_NAME));
loadButton.putClientProperty("JButton.buttonType", "segmentedTextured");
loadButton.putClientProperty("JButton.segmentPosition", "last");
toolBar.add(loadButton);
displayNodeButton = new JButton();
displayNodeButton.setAction(actionsMap.get(CaptureNodeAction.ACTION_NAME));
displayNodeButton.putClientProperty("JButton.buttonType", "segmentedTextured");
displayNodeButton.putClientProperty("JButton.segmentPosition", "first");
toolBar.add(displayNodeButton);
dumpDisplayListButton = new JButton();
dumpDisplayListButton.setAction(actionsMap.get(DumpDisplayListAction.ACTION_NAME));
dumpDisplayListButton.putClientProperty("JButton.buttonType", "segmentedTextured");
dumpDisplayListButton.putClientProperty("JButton.segmentPosition", "middle");
captureLayersButton = new JButton();
captureLayersButton.setAction(actionsMap.get(CaptureLayersAction.ACTION_NAME));
captureLayersButton.putClientProperty("JButton.buttonType", "segmentedTextured");
captureLayersButton.putClientProperty("JButton.segmentPosition", "middle");
toolBar.add(captureLayersButton);
invalidateButton = new JButton();
invalidateButton.setAction(actionsMap.get(InvalidateAction.ACTION_NAME));
invalidateButton.putClientProperty("JButton.buttonType", "segmentedTextured");
invalidateButton.putClientProperty("JButton.segmentPosition", "middle");
toolBar.add(invalidateButton);
requestLayoutButton = new JButton();
requestLayoutButton.setAction(actionsMap.get(RequestLayoutAction.ACTION_NAME));
requestLayoutButton.putClientProperty("JButton.buttonType", "segmentedTextured");
requestLayoutButton.putClientProperty("JButton.segmentPosition", "last");
toolBar.add(requestLayoutButton);
return toolBar;
}
private void setupProtocolDependentToolbar() {
// Some functionality is only enabled in certain versions of the protocol.
// Add/remove those buttons here
if (protocolVersion < 4) {
commandButtonsPanel.remove(dumpDisplayListButton);
} else if (dumpDisplayListButton.getParent() == null) {
commandButtonsPanel.add(dumpDisplayListButton,
commandButtonsPanel.getComponentCount() - 1);
}
}
private JMenuBar buildMenuBar() {
JMenuBar menuBar = new JMenuBar();
JMenu fileMenu = new JMenu();
JMenu viewMenu = new JMenu();
JMenu viewHierarchyMenu = new JMenu();
JMenu serverMenu = new JMenu();
saveMenuItem = new JMenuItem();
JMenuItem exitMenuItem = new JMenuItem();
showDevicesMenuItem = new JMenuItem();
loadMenuItem = new JMenuItem();
startMenuItem = new JMenuItem();
stopMenuItem = new JMenuItem();
fileMenu.setText("File");
saveMenuItem.setAction(actionsMap.get(SaveSceneAction.ACTION_NAME));
fileMenu.add(saveMenuItem);
exitMenuItem.setAction(actionsMap.get(ExitAction.ACTION_NAME));
fileMenu.add(exitMenuItem);
menuBar.add(fileMenu);
viewMenu.setText("View");
showDevicesMenuItem.setAction(actionsMap.get(ShowDevicesAction.ACTION_NAME));
showDevicesMenuItem.setEnabled(false);
viewMenu.add(showDevicesMenuItem);
menuBar.add(viewMenu);
viewHierarchyMenu.setText("Hierarchy");
loadMenuItem.setAction(actionsMap.get(LoadGraphAction.ACTION_NAME));
viewHierarchyMenu.add(loadMenuItem);
menuBar.add(viewHierarchyMenu);
serverMenu.setText("Server");
startMenuItem.setAction(actionsMap.get(StartServerAction.ACTION_NAME));
serverMenu.add(startMenuItem);
stopMenuItem.setAction(actionsMap.get(StopServerAction.ACTION_NAME));
serverMenu.add(stopMenuItem);
menuBar.add(serverMenu);
return menuBar;
}
private JComponent buildPixelPerfectPanel() {
JSplitPane splitter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
pixelPerfectTree = new JTree(new Object[0]);
pixelPerfectTree.setBorder(null);
pixelPerfectTree.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
pixelPerfectTree.addTreeSelectionListener(new TreeSelectionListener() {
public void valueChanged(TreeSelectionEvent event) {
ViewNode node = (ViewNode) event.getPath().getLastPathComponent();
screenViewer.select(node);
}
});
JScrollPane scroller = new JScrollPane(pixelPerfectTree);
scroller.setBorder(null);
scroller.getViewport().setBorder(null);
splitter.setContinuousLayout(true);
splitter.setLeftComponent(scroller);
splitter.setRightComponent(buildPixelPerfectViewer(splitter));
splitter.setBorder(null);
if (OS.isMacOsX() && OS.isLeopardOrLater()) {
splitter.setBorder(new UnifiedContentBorder());
}
return splitter;
}
private JComponent buildPixelPerfectViewer(JSplitPane splitter) {
screenViewer = new ScreenViewer(this, currentDevice, splitter.getDividerSize());
return screenViewer;
}
private void toggleGraphView() {
showStatusBarComponents();
screenViewer.stop();
mainPanel.remove(pixelPerfectPanel);
mainPanel.add(mainSplitter, BorderLayout.CENTER);
validate();
repaint();
}
private void showStatusBarComponents() {
viewCountLabel.setVisible(true);
zoomSlider.setVisible(true);
minZoomLabel.setVisible(true);
maxZoomLabel.setVisible(true);
filterLabel.setVisible(true);
filterText.setVisible(true);
}
private void togglePixelPerfectView() {
if (pixelPerfectPanel == null) {
pixelPerfectPanel = buildPixelPerfectPanel();
showPixelPerfectTree();
} else {
screenViewer.start();
}
hideStatusBarComponents();
mainPanel.remove(mainSplitter);
mainPanel.add(pixelPerfectPanel, BorderLayout.CENTER);
validate();
repaint();
}
private void zoomSliderStateChanged(ChangeEvent evt) {
JSlider slider = (JSlider) evt.getSource();
if (sceneView != null) {
scene.setZoomFactor(slider.getValue() / 100.0d);
sceneView.repaint();
}
}
private void showProperties(ViewNode node) {
propertiesTable.setModel(new PropertiesTableModel(node));
}
private void updateProfiles(double[] profiles) {
profilingTable.setModel(new ProfilesTableModel(profiles));
setVisibleRowCount(profilingTable, profiles.length + 1);
}
public static void setVisibleRowCount(JTable table, int rows) {
int height = 0;
for (int row = 0; row < rows; row++) {
height += table.getRowHeight(row);
}
Dimension size = new Dimension(table.getPreferredScrollableViewportSize().width, height);
table.setPreferredScrollableViewportSize(size);
table.revalidate();
}
private void showPixelPerfectTree() {
if (pixelPerfectTree == null) {
return;
}
pixelPerfectTree.setModel(new ViewsTreeModel(scene.getRoot()));
pixelPerfectTree.setCellRenderer(new ViewsTreeCellRenderer());
expandAll(pixelPerfectTree, true);
}
private static void expandAll(JTree tree, boolean expand) {
ViewNode root = (ViewNode) tree.getModel().getRoot();
expandAll(tree, new TreePath(root), expand);
}
private static void expandAll(JTree tree, TreePath parent, boolean expand) {
// Traverse children
ViewNode node = (ViewNode)parent.getLastPathComponent();
if (node.children != null) {
for (ViewNode n : node.children) {
TreePath path = parent.pathByAddingChild(n);
expandAll(tree, path, expand);
}
}
if (expand) {
tree.expandPath(parent);
} else {
tree.collapsePath(parent);
}
}
private void createGraph(ViewHierarchyScene scene) {
scene.addObjectSceneListener(new SceneFocusListener(),
ObjectSceneEventType.OBJECT_FOCUS_CHANGED);
if (mainSplitter == null) {
mainPanel.remove(deviceSelector);
mainPanel.add(buildGraphPanel(), BorderLayout.CENTER);
showDevicesButton.setEnabled(true);
showDevicesMenuItem.setEnabled(true);
graphViewButton.setEnabled(true);
pixelPerfectViewButton.setEnabled(true);
showStatusBarComponents();
}
sceneView = scene.createView();
sceneView.addMouseListener(new NodeClickListener());
sceneView.addMouseWheelListener(new WheelZoomListener());
sceneScroller.setViewportView(sceneView);
if (extrasPanel != null) {
sideSplitter.remove(extrasPanel);
}
sideSplitter.setBottomComponent(buildExtrasPanel());
mainSplitter.setDividerLocation(getWidth() - mainSplitter.getDividerSize() -
buttonsPanel.getPreferredSize().width);
captureLayersButton.setEnabled(true);
saveMenuItem.setEnabled(true);
showPixelPerfectTree();
updateStatus();
layoutScene();
}
private void layoutScene() {
TreeGraphLayout<ViewNode, String> layout =
new TreeGraphLayout<ViewNode, String>(scene, 50, 50, 70, 30, true);
layout.layout(scene.getRoot());
}
private void updateStatus() {
viewCountLabel.setText("" + scene.getNodes().size() + " views");
zoomSlider.setEnabled(scene.getNodes().size() > 0);
}
private JPanel buildExtrasPanel() {
extrasPanel = new JPanel(new BorderLayout());
JScrollPane p = new JScrollPane(layoutView = new LayoutRenderer(scene, sceneView));
JScrollBar b = p.getVerticalScrollBar();
b.setUnitIncrement(10);
extrasPanel.add(p);
extrasPanel.add(scene.createSatelliteView(), BorderLayout.SOUTH);
extrasPanel.add(buildLayoutViewControlButtons(), BorderLayout.NORTH);
return extrasPanel;
}
private JComponent buildLayoutViewControlButtons() {
buttonsPanel = new JToolBar();
buttonsPanel.setFloatable(false);
ButtonGroup group = new ButtonGroup();
JToggleButton white = new JToggleButton("On White");
toggleColorOnSelect(white);
white.putClientProperty("JButton.buttonType", "segmentedTextured");
white.putClientProperty("JButton.segmentPosition", "first");
white.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
layoutView.setBackground(Color.WHITE);
layoutView.setForeground(Color.BLACK);
}
});
group.add(white);
buttonsPanel.add(white);
JToggleButton black = new JToggleButton("On Black");
toggleColorOnSelect(black);
black.putClientProperty("JButton.buttonType", "segmentedTextured");
black.putClientProperty("JButton.segmentPosition", "last");
black.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
layoutView.setBackground(Color.BLACK);
layoutView.setForeground(Color.WHITE);
}
});
group.add(black);
buttonsPanel.add(black);
black.setSelected(true);
JCheckBox showExtras = new JCheckBox("Show Extras");
showExtras.putClientProperty("JComponent.sizeVariant", "small");
showExtras.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
layoutView.setShowExtras(((JCheckBox) e.getSource()).isSelected());
}
});
buttonsPanel.add(showExtras);
return buttonsPanel;
}
private void showCaptureWindow(ViewNode node, String captureParams, Image image) {
if (image != null) {
layoutView.repaint();
JFrame frame = new JFrame(captureParams);
JPanel panel = new JPanel(new BorderLayout());
final CaptureRenderer label = new CaptureRenderer(new ImageIcon(image), node);
label.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24));
final JPanel solidColor = new JPanel(new BorderLayout());
solidColor.setBackground(Color.BLACK);
solidColor.add(label);
JToolBar toolBar = new JToolBar();
toolBar.setFloatable(false);
ButtonGroup group = new ButtonGroup();
JToggleButton white = new JToggleButton("On White");
toggleColorOnSelect(white);
white.putClientProperty("JButton.buttonType", "segmentedTextured");
white.putClientProperty("JButton.segmentPosition", "first");
white.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
solidColor.setBackground(Color.WHITE);
}
});
group.add(white);
toolBar.add(white);
JToggleButton black = new JToggleButton("On Black");
toggleColorOnSelect(black);
black.putClientProperty("JButton.buttonType", "segmentedTextured");
black.putClientProperty("JButton.segmentPosition", "last");
black.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
solidColor.setBackground(Color.BLACK);
}
});
group.add(black);
toolBar.add(black);
black.setSelected(true);
JCheckBox showExtras = new JCheckBox("Show Extras");
showExtras.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
label.setShowExtras(((JCheckBox) e.getSource()).isSelected());
}
});
toolBar.add(showExtras);
panel.add(toolBar, BorderLayout.NORTH);
panel.add(solidColor);
frame.add(panel);
frame.pack();
frame.setResizable(false);
frame.setLocationRelativeTo(Workspace.this);
frame.setVisible(true);
}
}
private void reset() {
currentDevice = null;
currentWindow = null;
currentDeviceChanged();
windowsTableModel.setVisible(false);
windowsTableModel.clear();
showDevicesSelector();
}
public void showDevicesSelector() {
if (mainSplitter != null) {
if (pixelPerfectPanel != null) {
screenViewer.start();
}
mainPanel.remove(graphViewButton.isSelected() ? mainSplitter : pixelPerfectPanel);
mainPanel.add(deviceSelector, BorderLayout.CENTER);
pixelPerfectPanel = mainSplitter = null;
graphViewButton.setSelected(true);
hideStatusBarComponents();
saveMenuItem.setEnabled(false);
showDevicesMenuItem.setEnabled(false);
showDevicesButton.setEnabled(false);
displayNodeButton.setEnabled(false);
captureLayersButton.setEnabled(false);
invalidateButton.setEnabled(false);
dumpDisplayListButton.setEnabled(false);
requestLayoutButton.setEnabled(false);
graphViewButton.setEnabled(false);
pixelPerfectViewButton.setEnabled(false);
if (currentDevice != null) {
if (!DeviceBridge.isViewServerRunning(currentDevice)) {
DeviceBridge.startViewServer(currentDevice);
}
loadWindows().execute();
windowsTableModel.setVisible(true);
}
validate();
repaint();
}
}
private void currentDeviceChanged() {
if (currentDevice == null) {
startButton.setEnabled(false);
startMenuItem.setEnabled(false);
stopButton.setEnabled(false);
stopMenuItem.setEnabled(false);
refreshButton.setEnabled(false);
saveMenuItem.setEnabled(false);
loadButton.setEnabled(false);
displayNodeButton.setEnabled(false);
captureLayersButton.setEnabled(false);
invalidateButton.setEnabled(false);
dumpDisplayListButton.setEnabled(false);
graphViewButton.setEnabled(false);
pixelPerfectViewButton.setEnabled(false);
requestLayoutButton.setEnabled(false);
loadMenuItem.setEnabled(false);
} else {
loadMenuItem.setEnabled(true);
checkForServerOnCurrentDevice();
}
}
private void checkForServerOnCurrentDevice() {
if (DeviceBridge.isViewServerRunning(currentDevice)) {
startButton.setEnabled(false);
startMenuItem.setEnabled(false);
stopButton.setEnabled(true);
stopMenuItem.setEnabled(true);
loadButton.setEnabled(true);
refreshButton.setEnabled(true);
} else {
startButton.setEnabled(true);
startMenuItem.setEnabled(true);
stopButton.setEnabled(false);
stopMenuItem.setEnabled(false);
loadButton.setEnabled(false);
refreshButton.setEnabled(false);
}
}
public void cleanupDevices() {
for (IDevice device : devicesTableModel.getDevices()) {
DeviceBridge.removeDeviceForward(device);
}
}
private static void toggleColorOnSelect(JToggleButton button) {
if (!OS.isMacOsX() || !OS.isLeopardOrLater()) {
return;
}
button.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent event) {
JToggleButton button = (JToggleButton) event.getSource();
if (button.isSelected()) {
button.setForeground(Color.WHITE);
} else {
button.setForeground(Color.BLACK);
}
}
});
}
private void updateFilter(DocumentEvent e) {
final Document document = e.getDocument();
try {
updateFilteredNodes(document.getText(0, document.getLength()));
} catch (BadLocationException e1) {
e1.printStackTrace();
}
}
private void updateFilteredNodes(String filterText) {
final ViewNode root = scene.getRoot();
try {
final Pattern pattern = Pattern.compile(filterText, Pattern.CASE_INSENSITIVE);
filterNodes(pattern, root);
} catch (PatternSyntaxException e) {
filterNodes(null, root);
}
repaint();
}
private void filterNodes(Pattern pattern, ViewNode root) {
root.filter(pattern);
for (ViewNode node : root.children) {
filterNodes(pattern, node);
}
}
public void beginTask() {
progress.setVisible(true);
}
public void endTask() {
progress.setVisible(false);
}
public SwingWorker<?, ?> showNodeCapture() {
if (scene.getFocusedObject() == null) {
return null;
}
return new CaptureNodeTask();
}
public SwingWorker<?, ?> outputDisplayList() {
if (scene.getFocusedObject() == null) {
return null;
}
return new DumpDisplayListTask();
}
public SwingWorker<?, ?> captureLayers() {
JFileChooser chooser = new JFileChooser();
chooser.setFileFilter(new PsdFileFilter());
int choice = chooser.showSaveDialog(sceneView);
if (choice == JFileChooser.APPROVE_OPTION) {
return new CaptureLayersTask(chooser.getSelectedFile());
} else {
return null;
}
}
public SwingWorker<?, ?> startServer() {
return new StartServerTask();
}
public SwingWorker<?, ?> stopServer() {
return new StopServerTask();
}
public SwingWorker<?, ?> loadWindows() {
return new LoadWindowsTask();
}
public SwingWorker<?, ?> loadGraph() {
return new LoadGraphTask();
}
public SwingWorker<?, ?> invalidateView() {
if (scene.getFocusedObject() == null) {
return null;
}
return new InvalidateTask();
}
public SwingWorker<?, ?> requestLayout() {
if (scene.getFocusedObject() == null) {
return null;
}
return new RequestLayoutTask();
}
public SwingWorker<?, ?> saveSceneAsImage() {
JFileChooser chooser = new JFileChooser();
chooser.setFileFilter(new PngFileFilter());
int choice = chooser.showSaveDialog(sceneView);
if (choice == JFileChooser.APPROVE_OPTION) {
return new SaveSceneTask(chooser.getSelectedFile());
} else {
return null;
}
}
private class InvalidateTask extends SwingWorker<Object, Void> {
private String captureParams;
private InvalidateTask() {
captureParams = scene.getFocusedObject().toString();
beginTask();
}
@Override
@WorkerThread
protected Object doInBackground() throws Exception {
ViewManager.invalidate(currentDevice, currentWindow, captureParams);
return null;
}
@Override
protected void done() {
endTask();
}
}
private class DumpDisplayListTask extends SwingWorker<Object, Void> {
private String captureParams;
private DumpDisplayListTask() {
captureParams = scene.getFocusedObject().toString();
beginTask();
}
@Override
@WorkerThread
protected Object doInBackground() throws Exception {
ViewManager.outputDisplayList(currentDevice, currentWindow, captureParams);
return null;
}
@Override
protected void done() {
endTask();
}
}
private class RequestLayoutTask extends SwingWorker<Object, Void> {
private String captureParams;
private RequestLayoutTask() {
captureParams = scene.getFocusedObject().toString();
beginTask();
}
@Override
@WorkerThread
protected Object doInBackground() throws Exception {
ViewManager.requestLayout(currentDevice, currentWindow, captureParams);
return null;
}
@Override
protected void done() {
endTask();
}
}
private class CaptureLayersTask extends SwingWorker<Boolean, Void> {
private File file;
private CaptureLayersTask(File file) {
this.file = file;
beginTask();
}
@Override
@WorkerThread
protected Boolean doInBackground() throws Exception {
return CaptureLoader.saveLayers(currentDevice, currentWindow, file);
}
@Override
protected void done() {
endTask();
}
}
private class CaptureNodeTask extends SwingWorker<Image, Void> {
private String captureParams;
private ViewNode node;
private CaptureNodeTask() {
node = (ViewNode) scene.getFocusedObject();
captureParams = node.toString();
beginTask();
}
@Override
@WorkerThread
protected Image doInBackground() throws Exception {
node.image = CaptureLoader.loadCapture(currentDevice, currentWindow, captureParams);
return node.image;
}
@Override
protected void done() {
try {
Image image = get();
showCaptureWindow(node, captureParams, image);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
endTask();
}
}
}
static class WindowsResult {
Window[] windows;
int serverVersion;
int protocolVersion;
}
private class LoadWindowsTask extends SwingWorker<WindowsResult, Void> {
private LoadWindowsTask() {
beginTask();
}
@Override
@WorkerThread
protected WindowsResult doInBackground() throws Exception {
WindowsResult r = new WindowsResult();
r.protocolVersion = VersionLoader.loadProtocolVersion(currentDevice);
r.serverVersion = VersionLoader.loadServerVersion(currentDevice);
r.windows = WindowsLoader.loadWindows(currentDevice,
r.protocolVersion, r.serverVersion);
return r;
}
@Override
protected void done() {
try {
WindowsResult result = get();
protocolVersion = result.protocolVersion;
serverVersion = result.serverVersion;
setupProtocolDependentToolbar();
windowsTableModel.clear();
windowsTableModel.addWindows(result.windows);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
endTask();
}
}
}
private class StartServerTask extends SwingWorker<Object, Void> {
public StartServerTask() {
beginTask();
}
@Override
@WorkerThread
protected Object doInBackground() {
DeviceBridge.startViewServer(currentDevice);
return null;
}
@Override
protected void done() {
new LoadWindowsTask().execute();
windowsTableModel.setVisible(true);
checkForServerOnCurrentDevice();
endTask();
}
}
private class StopServerTask extends SwingWorker<Object, Void> {
public StopServerTask() {
beginTask();
}
@Override
@WorkerThread
protected Object doInBackground() {
DeviceBridge.stopViewServer(currentDevice);
return null;
}
@Override
protected void done() {
windowsTableModel.setVisible(false);
windowsTableModel.clear();
checkForServerOnCurrentDevice();
endTask();
}
}
private class LoadGraphTask extends SwingWorker<double[], Void> {
public LoadGraphTask() {
beginTask();
}
@Override
@WorkerThread
protected double[] doInBackground() {
scene = ViewHierarchyLoader.loadScene(currentDevice, currentWindow);
return ProfilesLoader.loadProfiles(currentDevice, currentWindow,
scene.getRoot().toString());
}
@Override
protected void done() {
try {
createGraph(scene);
updateProfiles(get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
endTask();
}
}
}
private class SaveSceneTask extends SwingWorker<Object, Void> {
private File file;
private SaveSceneTask(File file) {
this.file = file;
beginTask();
}
@Override
@WorkerThread
protected Object doInBackground() {
if (sceneView == null) {
return null;
}
try {
BufferedImage image = new BufferedImage(sceneView.getWidth(),
sceneView.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = image.createGraphics();
sceneView.paint(g2);
g2.dispose();
ImageIO.write(image, "PNG", file);
} catch (IOException ex) {
ex.printStackTrace();
}
return null;
}
@Override
protected void done() {
endTask();
}
}
private class SceneFocusListener implements ObjectSceneListener {
public void objectAdded(ObjectSceneEvent arg0, Object arg1) {
}
public void objectRemoved(ObjectSceneEvent arg0, Object arg1) {
}
public void objectStateChanged(ObjectSceneEvent arg0, Object arg1,
ObjectState arg2, ObjectState arg3) {
}
public void selectionChanged(ObjectSceneEvent e, Set<Object> previousSelection,
Set<Object> newSelection) {
}
public void highlightingChanged(ObjectSceneEvent arg0, Set<Object> arg1, Set<Object> arg2) {
}
public void hoverChanged(ObjectSceneEvent arg0, Object arg1, Object arg2) {
}
public void focusChanged(ObjectSceneEvent e, Object oldFocus, Object newFocus) {
displayNodeButton.setEnabled(true);
invalidateButton.setEnabled(true);
dumpDisplayListButton.setEnabled(true);
requestLayoutButton.setEnabled(true);
Set<Object> selection = new HashSet<Object>();
selection.add(newFocus);
scene.setSelectedObjects(selection);
showProperties((ViewNode) newFocus);
layoutView.repaint();
}
}
private class NodeClickListener extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
showNodeCapture().execute();
}
}
}
private class WheelZoomListener implements MouseWheelListener {
public void mouseWheelMoved(MouseWheelEvent e) {
if (zoomSlider != null) {
int val = zoomSlider.getValue();
val -= e.getWheelRotation() * 10;
zoomSlider.setValue(val);
}
}
}
private class DevicesTableModel extends DefaultTableModel implements
AndroidDebugBridge.IDeviceChangeListener {
private ArrayList<IDevice> devices;
private DevicesTableModel() {
devices = new ArrayList<IDevice>();
}
@Override
public int getColumnCount() {
return 1;
}
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
@Override
public Object getValueAt(int row, int column) {
return devices.get(row);
}
@Override
public String getColumnName(int column) {
return "Devices";
}
@WorkerThread
public void deviceConnected(final IDevice device) {
DeviceBridge.setupDeviceForward(device);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
addDevice(device);
}
});
}
@WorkerThread
public void deviceDisconnected(final IDevice device) {
DeviceBridge.removeDeviceForward(device);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
removeDevice(device);
}
});
}
public void addDevice(IDevice device) {
if (!devices.contains(device)) {
devices.add(device);
fireTableDataChanged();
}
}
public void removeDevice(IDevice device) {
if (device.equals(currentDevice)) {
reset();
}
if (devices.contains(device)) {
devices.remove(device);
fireTableDataChanged();
}
}
@WorkerThread
public void deviceChanged(IDevice device, int changeMask) {
if ((changeMask & IDevice.CHANGE_STATE) != 0 &&
device.isOnline()) {
// if the device state changed and it's now online, we set up its port forwarding.
DeviceBridge.setupDeviceForward(device);
} else if (device == currentDevice && (changeMask & IDevice.CHANGE_CLIENT_LIST) != 0) {
// if the changed device is the current one and the client list changed, we update
// the UI.
loadWindows().execute();
windowsTableModel.setVisible(true);
}
}
@Override
public int getRowCount() {
return devices == null ? 0 : devices.size();
}
public IDevice getDevice(int index) {
return index < devices.size() ? devices.get(index) : null;
}
public IDevice[] getDevices() {
return devices.toArray(new IDevice[devices.size()]);
}
}
private static class WindowsTableModel extends DefaultTableModel {
private ArrayList<Window> windows;
private boolean visible;
private WindowsTableModel() {
windows = new ArrayList<Window>();
windows.add(Window.FOCUSED_WINDOW);
}
@Override
public int getColumnCount() {
return 1;
}
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
@Override
public String getColumnName(int column) {
return "Windows";
}
@Override
public Object getValueAt(int row, int column) {
return windows.get(row);
}
@Override
public int getRowCount() {
return !visible || windows == null ? 0 : windows.size();
}
public void setVisible(boolean visible) {
this.visible = visible;
fireTableDataChanged();
}
public void addWindow(Window window) {
windows.add(window);
fireTableDataChanged();
}
public void addWindows(Window[] windowsList) {
//noinspection ManualArrayToCollectionCopy
for (Window window : windowsList) {
windows.add(window);
}
fireTableDataChanged();
}
public void clear() {
windows.clear();
windows.add(Window.FOCUSED_WINDOW);
}
public Window getWindow(int index) {
return windows.get(index);
}
}
private class DeviceSelectedListener implements ListSelectionListener {
public void valueChanged(ListSelectionEvent event) {
if (event.getValueIsAdjusting()) {
return;
}
int row = devices.getSelectedRow();
if (row >= 0) {
currentDevice = devicesTableModel.getDevice(row);
currentDeviceChanged();
if (currentDevice != null) {
if (!DeviceBridge.isViewServerRunning(currentDevice)) {
DeviceBridge.startViewServer(currentDevice);
checkForServerOnCurrentDevice();
}
loadWindows().execute();
windowsTableModel.setVisible(true);
}
} else {
currentDevice = null;
currentDeviceChanged();
windowsTableModel.setVisible(false);
windowsTableModel.clear();
}
}
}
private class WindowSelectedListener implements ListSelectionListener {
public void valueChanged(ListSelectionEvent event) {
if (event.getValueIsAdjusting()) {
return;
}
int row = windows.getSelectedRow();
if (row >= 0) {
currentWindow = windowsTableModel.getWindow(row);
} else {
currentWindow = Window.FOCUSED_WINDOW;
}
}
}
private static class ViewsTreeCellRenderer extends DefaultTreeCellRenderer {
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected,
boolean expanded, boolean leaf, int row, boolean hasFocus) {
final String name = ((ViewNode) value).name;
value = name.substring(name.lastIndexOf('.') + 1, name.lastIndexOf('@'));
return super.getTreeCellRendererComponent(tree, value, selected, expanded,
leaf, row, hasFocus);
}
}
}