blob: c5f3e598abc7f610742c0353ae3c4777c81485cd [file] [log] [blame]
/*
* Copyright (C) 2012 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.uiautomator;
import com.android.uiautomator.actions.ExpandAllAction;
import com.android.uiautomator.actions.ToggleNafAction;
import com.android.uiautomator.tree.AttributePair;
import com.android.uiautomator.tree.BasicTreeNode;
import com.android.uiautomator.tree.BasicTreeNodeContentProvider;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.layout.TableColumnLayout;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.ColumnWeightData;
import org.eclipse.jface.viewers.EditingSupport;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.jface.viewers.TextCellEditor;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.ImageLoader;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.Transform;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.Tree;
import java.io.File;
public class UiAutomatorView extends Composite {
private static final int IMG_BORDER = 2;
// The screenshot area is made of a stack layout of two components: screenshot canvas and
// a "specify screenshot" button. If a screenshot is already available, then that is displayed
// on the canvas. If it is not availble, then the "specify screenshot" button is displayed.
private Composite mScreenshotComposite;
private StackLayout mStackLayout;
private Composite mSetScreenshotComposite;
private Canvas mScreenshotCanvas;
private TreeViewer mTreeViewer;
private TableViewer mTableViewer;
private float mScale = 1.0f;
private int mDx, mDy;
private UiAutomatorModel mModel;
private File mModelFile;
private Image mScreenshot;
public UiAutomatorView(Composite parent, int style) {
super(parent, SWT.NONE);
setLayout(new FillLayout());
SashForm baseSash = new SashForm(this, SWT.HORIZONTAL);
mScreenshotComposite = new Composite(baseSash, SWT.BORDER);
mStackLayout = new StackLayout();
mScreenshotComposite.setLayout(mStackLayout);
// draw the canvas with border, so the divider area for sash form can be highlighted
mScreenshotCanvas = new Canvas(mScreenshotComposite, SWT.BORDER);
mStackLayout.topControl = mScreenshotCanvas;
mScreenshotComposite.layout();
mScreenshotCanvas.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
if (mModel != null) {
mModel.toggleExploreMode();
redrawScreenshot();
}
}
});
mScreenshotCanvas.setBackground(
getShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
mScreenshotCanvas.addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
if (mScreenshot != null) {
updateScreenshotTransformation();
// shifting the image here, so that there's a border around screen shot
// this makes highlighting red rectangles on the screen shot edges more visible
Transform t = new Transform(e.gc.getDevice());
t.translate(mDx, mDy);
t.scale(mScale, mScale);
e.gc.setTransform(t);
e.gc.drawImage(mScreenshot, 0, 0);
// this resets the transformation to identity transform, i.e. no change
// we don't use transformation here because it will cause the line pattern
// and line width of highlight rect to be scaled, causing to appear to be blurry
e.gc.setTransform(null);
if (mModel.shouldShowNafNodes()) {
// highlight the "Not Accessibility Friendly" nodes
e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW));
e.gc.setBackground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW));
for (Rectangle r : mModel.getNafNodes()) {
e.gc.setAlpha(50);
e.gc.fillRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y),
getScaledSize(r.width), getScaledSize(r.height));
e.gc.setAlpha(255);
e.gc.setLineStyle(SWT.LINE_SOLID);
e.gc.setLineWidth(2);
e.gc.drawRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y),
getScaledSize(r.width), getScaledSize(r.height));
}
}
// draw the mouseover rects
Rectangle rect = mModel.getCurrentDrawingRect();
if (rect != null) {
e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_RED));
if (mModel.isExploreMode()) {
// when we highlight nodes dynamically on mouse move,
// use dashed borders
e.gc.setLineStyle(SWT.LINE_DASH);
e.gc.setLineWidth(1);
} else {
// when highlighting nodes on tree node selection,
// use solid borders
e.gc.setLineStyle(SWT.LINE_SOLID);
e.gc.setLineWidth(2);
}
e.gc.drawRectangle(mDx + getScaledSize(rect.x), mDy + getScaledSize(rect.y),
getScaledSize(rect.width), getScaledSize(rect.height));
}
}
}
});
mScreenshotCanvas.addMouseMoveListener(new MouseMoveListener() {
@Override
public void mouseMove(MouseEvent e) {
if (mModel != null && mModel.isExploreMode()) {
BasicTreeNode node = mModel.updateSelectionForCoordinates(
getInverseScaledSize(e.x - mDx),
getInverseScaledSize(e.y - mDy));
if (node != null) {
updateTreeSelection(node);
}
}
}
});
mSetScreenshotComposite = new Composite(mScreenshotComposite, SWT.NONE);
mSetScreenshotComposite.setLayout(new GridLayout());
final Button setScreenshotButton = new Button(mSetScreenshotComposite, SWT.PUSH);
setScreenshotButton.setText("Specify Screenshot...");
setScreenshotButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent arg0) {
FileDialog fd = new FileDialog(setScreenshotButton.getShell());
fd.setFilterExtensions(new String[] { "*.png" });
if (mModelFile != null) {
fd.setFilterPath(mModelFile.getParent());
}
String screenshotPath = fd.open();
if (screenshotPath == null) {
return;
}
ImageData[] data;
try {
data = new ImageLoader().load(screenshotPath);
} catch (Exception e) {
return;
}
// "data" is an array, probably used to handle images that has multiple frames
// i.e. gifs or icons, we just care if it has at least one here
if (data.length < 1) {
return;
}
mScreenshot = new Image(Display.getDefault(), data[0]);
redrawScreenshot();
}
});
// right sash is split into 2 parts: upper-right and lower-right
// both are composites with borders, so that the horizontal divider can be highlighted by
// the borders
SashForm rightSash = new SashForm(baseSash, SWT.VERTICAL);
// upper-right base contains the toolbar and the tree
Composite upperRightBase = new Composite(rightSash, SWT.BORDER);
upperRightBase.setLayout(new GridLayout(1, false));
ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
toolBarManager.add(new ExpandAllAction(this));
toolBarManager.add(new ToggleNafAction(this));
toolBarManager.createControl(upperRightBase);
mTreeViewer = new TreeViewer(upperRightBase, SWT.NONE);
mTreeViewer.setContentProvider(new BasicTreeNodeContentProvider());
// default LabelProvider uses toString() to generate text to display
mTreeViewer.setLabelProvider(new LabelProvider());
mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
@Override
public void selectionChanged(SelectionChangedEvent event) {
BasicTreeNode selectedNode = null;
if (event.getSelection() instanceof IStructuredSelection) {
IStructuredSelection selection = (IStructuredSelection) event.getSelection();
Object o = selection.getFirstElement();
if (o instanceof BasicTreeNode) {
selectedNode = (BasicTreeNode) o;
}
}
mModel.setSelectedNode(selectedNode);
redrawScreenshot();
if (selectedNode != null) {
loadAttributeTable();
}
}
});
Tree tree = mTreeViewer.getTree();
tree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
// move focus so that it's not on tool bar (looks weird)
tree.setFocus();
// lower-right base contains the detail group
Composite lowerRightBase = new Composite(rightSash, SWT.BORDER);
lowerRightBase.setLayout(new FillLayout());
Group grpNodeDetail = new Group(lowerRightBase, SWT.NONE);
grpNodeDetail.setLayout(new FillLayout(SWT.HORIZONTAL));
grpNodeDetail.setText("Node Detail");
Composite tableContainer = new Composite(grpNodeDetail, SWT.NONE);
TableColumnLayout columnLayout = new TableColumnLayout();
tableContainer.setLayout(columnLayout);
mTableViewer = new TableViewer(tableContainer, SWT.NONE | SWT.FULL_SELECTION);
Table table = mTableViewer.getTable();
table.setLinesVisible(true);
// use ArrayContentProvider here, it assumes the input to the TableViewer
// is an array, where each element represents a row in the table
mTableViewer.setContentProvider(new ArrayContentProvider());
TableViewerColumn tableViewerColumnKey = new TableViewerColumn(mTableViewer, SWT.NONE);
TableColumn tblclmnKey = tableViewerColumnKey.getColumn();
tableViewerColumnKey.setLabelProvider(new ColumnLabelProvider() {
@Override
public String getText(Object element) {
if (element instanceof AttributePair) {
// first column, shows the attribute name
return ((AttributePair)element).key;
}
return super.getText(element);
}
});
columnLayout.setColumnData(tblclmnKey,
new ColumnWeightData(1, ColumnWeightData.MINIMUM_WIDTH, true));
TableViewerColumn tableViewerColumnValue = new TableViewerColumn(mTableViewer, SWT.NONE);
tableViewerColumnValue.setEditingSupport(new AttributeTableEditingSupport(mTableViewer));
TableColumn tblclmnValue = tableViewerColumnValue.getColumn();
columnLayout.setColumnData(tblclmnValue,
new ColumnWeightData(2, ColumnWeightData.MINIMUM_WIDTH, true));
tableViewerColumnValue.setLabelProvider(new ColumnLabelProvider() {
@Override
public String getText(Object element) {
if (element instanceof AttributePair) {
// second column, shows the attribute value
return ((AttributePair)element).value;
}
return super.getText(element);
}
});
// sets the ratio of the vertical split: left 5 vs right 3
baseSash.setWeights(new int[]{5, 3});
}
private int getScaledSize(int size) {
if (mScale == 1.0f) {
return size;
} else {
return new Double(Math.floor((size * mScale))).intValue();
}
}
private int getInverseScaledSize(int size) {
if (mScale == 1.0f) {
return size;
} else {
return new Double(Math.floor((size / mScale))).intValue();
}
}
private void updateScreenshotTransformation() {
Rectangle canvas = mScreenshotCanvas.getBounds();
Rectangle image = mScreenshot.getBounds();
float scaleX = (canvas.width - 2 * IMG_BORDER - 1) / (float)image.width;
float scaleY = (canvas.height - 2 * IMG_BORDER - 1) / (float)image.height;
// use the smaller scale here so that we can fit the entire screenshot
mScale = Math.min(scaleX, scaleY);
// calculate translation values to center the image on the canvas
mDx = (canvas.width - getScaledSize(image.width) - IMG_BORDER * 2) / 2 + IMG_BORDER;
mDy = (canvas.height - getScaledSize(image.height) - IMG_BORDER * 2) / 2 + IMG_BORDER;
}
private class AttributeTableEditingSupport extends EditingSupport {
private TableViewer mViewer;
public AttributeTableEditingSupport(TableViewer viewer) {
super(viewer);
mViewer = viewer;
}
@Override
protected boolean canEdit(Object arg0) {
return true;
}
@Override
protected CellEditor getCellEditor(Object arg0) {
return new TextCellEditor(mViewer.getTable());
}
@Override
protected Object getValue(Object o) {
return ((AttributePair)o).value;
}
@Override
protected void setValue(Object arg0, Object arg1) {
}
}
/**
* Causes a redraw of the canvas.
*
* The drawing code of canvas will handle highlighted nodes and etc based on data
* retrieved from Model
*/
public void redrawScreenshot() {
if (mScreenshot == null) {
mStackLayout.topControl = mSetScreenshotComposite;
} else {
mStackLayout.topControl = mScreenshotCanvas;
}
mScreenshotComposite.layout();
mScreenshotCanvas.redraw();
}
public void setInputHierarchy(Object input) {
mTreeViewer.setInput(input);
}
public void loadAttributeTable() {
// udpate the lower right corner table to show the attributes of the node
mTableViewer.setInput(mModel.getSelectedNode().getAttributesArray());
}
public void expandAll() {
mTreeViewer.expandAll();
}
public void updateTreeSelection(BasicTreeNode node) {
mTreeViewer.setSelection(new StructuredSelection(node), true);
}
public void setModel(UiAutomatorModel model, File modelBackingFile, Image screenshot) {
mModel = model;
mModelFile = modelBackingFile;
if (mScreenshot != null) {
mScreenshot.dispose();
}
mScreenshot = screenshot;
redrawScreenshot();
// load xml into tree
BasicTreeNode wrapper = new BasicTreeNode();
// putting another root node on top of existing root node
// because Tree seems to like to hide the root node
wrapper.addChild(mModel.getXmlRootNode());
setInputHierarchy(wrapper);
mTreeViewer.getTree().setFocus();
}
public boolean shouldShowNafNodes() {
return mModel != null ? mModel.shouldShowNafNodes() : false;
}
public void toggleShowNaf() {
if (mModel != null) {
mModel.toggleShowNaf();
}
}
}