1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.uiautomator; 18 19 import com.android.uiautomator.actions.ExpandAllAction; 20 import com.android.uiautomator.actions.OpenFilesAction; 21 import com.android.uiautomator.actions.ScreenshotAction; 22 import com.android.uiautomator.actions.ToggleNafAction; 23 import com.android.uiautomator.tree.AttributePair; 24 import com.android.uiautomator.tree.BasicTreeNode; 25 import com.android.uiautomator.tree.BasicTreeNodeContentProvider; 26 27 import org.eclipse.jface.action.Action; 28 import org.eclipse.jface.action.ToolBarManager; 29 import org.eclipse.jface.layout.TableColumnLayout; 30 import org.eclipse.jface.viewers.ArrayContentProvider; 31 import org.eclipse.jface.viewers.CellEditor; 32 import org.eclipse.jface.viewers.ColumnLabelProvider; 33 import org.eclipse.jface.viewers.ColumnWeightData; 34 import org.eclipse.jface.viewers.EditingSupport; 35 import org.eclipse.jface.viewers.ISelectionChangedListener; 36 import org.eclipse.jface.viewers.IStructuredSelection; 37 import org.eclipse.jface.viewers.LabelProvider; 38 import org.eclipse.jface.viewers.SelectionChangedEvent; 39 import org.eclipse.jface.viewers.StructuredSelection; 40 import org.eclipse.jface.viewers.TableViewer; 41 import org.eclipse.jface.viewers.TableViewerColumn; 42 import org.eclipse.jface.viewers.TextCellEditor; 43 import org.eclipse.jface.viewers.TreeViewer; 44 import org.eclipse.jface.window.ApplicationWindow; 45 import org.eclipse.swt.SWT; 46 import org.eclipse.swt.custom.SashForm; 47 import org.eclipse.swt.events.MouseAdapter; 48 import org.eclipse.swt.events.MouseEvent; 49 import org.eclipse.swt.events.MouseMoveListener; 50 import org.eclipse.swt.events.PaintEvent; 51 import org.eclipse.swt.events.PaintListener; 52 import org.eclipse.swt.graphics.Image; 53 import org.eclipse.swt.graphics.Point; 54 import org.eclipse.swt.graphics.Rectangle; 55 import org.eclipse.swt.graphics.Transform; 56 import org.eclipse.swt.layout.FillLayout; 57 import org.eclipse.swt.layout.GridData; 58 import org.eclipse.swt.layout.GridLayout; 59 import org.eclipse.swt.widgets.Canvas; 60 import org.eclipse.swt.widgets.Composite; 61 import org.eclipse.swt.widgets.Control; 62 import org.eclipse.swt.widgets.Group; 63 import org.eclipse.swt.widgets.Shell; 64 import org.eclipse.swt.widgets.Table; 65 import org.eclipse.swt.widgets.TableColumn; 66 import org.eclipse.swt.widgets.Tree; 67 68 public class UiAutomatorViewer extends ApplicationWindow { 69 70 private static final int IMG_BORDER = 2; 71 72 private Canvas mScreenshotCanvas; 73 private TreeViewer mTreeViewer; 74 75 private Action mOpenFilesAction; 76 private Action mExpandAllAction; 77 private Action mScreenshotAction; 78 private Action mToggleNafAction; 79 private TableViewer mTableViewer; 80 81 private float mScale = 1.0f; 82 private int mDx, mDy; 83 84 /** 85 * Create the application window. 86 */ 87 public UiAutomatorViewer() { 88 super(null); 89 UiAutomatorModel.createInstance(this); 90 createActions(); 91 } 92 93 /** 94 * Create contents of the application window. 95 * 96 * @param parent 97 */ 98 @Override 99 protected Control createContents(Composite parent) { 100 SashForm baseSash = new SashForm(parent, SWT.HORIZONTAL | SWT.NONE); 101 // draw the canvas with border, so the divider area for sash form can be highlighted 102 mScreenshotCanvas = new Canvas(baseSash, SWT.BORDER); 103 mScreenshotCanvas.addMouseListener(new MouseAdapter() { 104 @Override 105 public void mouseUp(MouseEvent e) { 106 UiAutomatorModel.getModel().toggleExploreMode(); 107 } 108 }); 109 mScreenshotCanvas.setBackground( 110 getShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND)); 111 mScreenshotCanvas.addPaintListener(new PaintListener() { 112 @Override 113 public void paintControl(PaintEvent e) { 114 Image image = UiAutomatorModel.getModel().getScreenshot(); 115 if (image != null) { 116 updateScreenshotTransformation(); 117 // shifting the image here, so that there's a border around screen shot 118 // this makes highlighting red rectangles on the screen shot edges more visible 119 Transform t = new Transform(e.gc.getDevice()); 120 t.translate(mDx, mDy); 121 t.scale(mScale, mScale); 122 e.gc.setTransform(t); 123 e.gc.drawImage(image, 0, 0); 124 // this resets the transformation to identity transform, i.e. no change 125 // we don't use transformation here because it will cause the line pattern 126 // and line width of highlight rect to be scaled, causing to appear to be blurry 127 e.gc.setTransform(null); 128 if (UiAutomatorModel.getModel().shouldShowNafNodes()) { 129 // highlight the "Not Accessibility Friendly" nodes 130 e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW)); 131 e.gc.setBackground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW)); 132 for (Rectangle r : UiAutomatorModel.getModel().getNafNodes()) { 133 e.gc.setAlpha(50); 134 e.gc.fillRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y), 135 getScaledSize(r.width), getScaledSize(r.height)); 136 e.gc.setAlpha(255); 137 e.gc.setLineStyle(SWT.LINE_SOLID); 138 e.gc.setLineWidth(2); 139 e.gc.drawRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y), 140 getScaledSize(r.width), getScaledSize(r.height)); 141 } 142 } 143 // draw the mouseover rects 144 Rectangle rect = UiAutomatorModel.getModel().getCurrentDrawingRect(); 145 if (rect != null) { 146 e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_RED)); 147 if (UiAutomatorModel.getModel().isExploreMode()) { 148 // when we highlight nodes dynamically on mouse move, 149 // use dashed borders 150 e.gc.setLineStyle(SWT.LINE_DASH); 151 e.gc.setLineWidth(1); 152 } else { 153 // when highlighting nodes on tree node selection, 154 // use solid borders 155 e.gc.setLineStyle(SWT.LINE_SOLID); 156 e.gc.setLineWidth(2); 157 } 158 e.gc.drawRectangle(mDx + getScaledSize(rect.x), mDy + getScaledSize(rect.y), 159 getScaledSize(rect.width), getScaledSize(rect.height)); 160 } 161 } 162 } 163 }); 164 mScreenshotCanvas.addMouseMoveListener(new MouseMoveListener() { 165 @Override 166 public void mouseMove(MouseEvent e) { 167 if (UiAutomatorModel.getModel().isExploreMode()) { 168 UiAutomatorModel.getModel().updateSelectionForCoordinates( 169 getInverseScaledSize(e.x - mDx), 170 getInverseScaledSize(e.y - mDy)); 171 } 172 } 173 }); 174 175 // right sash is split into 2 parts: upper-right and lower-right 176 // both are composites with borders, so that the horizontal divider can be highlighted by 177 // the borders 178 SashForm rightSash = new SashForm(baseSash, SWT.VERTICAL); 179 180 // upper-right base contains the toolbar and the tree 181 Composite upperRightBase = new Composite(rightSash, SWT.BORDER); 182 upperRightBase.setLayout(new GridLayout(1, false)); 183 ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT); 184 toolBarManager.add(mOpenFilesAction); 185 toolBarManager.add(mExpandAllAction); 186 toolBarManager.add(mScreenshotAction); 187 toolBarManager.add(mToggleNafAction); 188 toolBarManager.createControl(upperRightBase); 189 190 mTreeViewer = new TreeViewer(upperRightBase, SWT.NONE); 191 mTreeViewer.setContentProvider(new BasicTreeNodeContentProvider()); 192 // default LabelProvider uses toString() to generate text to display 193 mTreeViewer.setLabelProvider(new LabelProvider()); 194 mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() { 195 @Override 196 public void selectionChanged(SelectionChangedEvent event) { 197 if (event.getSelection().isEmpty()) { 198 UiAutomatorModel.getModel().setSelectedNode(null); 199 } else if (event.getSelection() instanceof IStructuredSelection) { 200 IStructuredSelection selection = (IStructuredSelection) event.getSelection(); 201 Object o = selection.toArray()[0]; 202 if (o instanceof BasicTreeNode) { 203 UiAutomatorModel.getModel().setSelectedNode((BasicTreeNode)o); 204 } 205 } 206 } 207 }); 208 Tree tree = mTreeViewer.getTree(); 209 tree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); 210 // move focus so that it's not on tool bar (looks weird) 211 tree.setFocus(); 212 213 // lower-right base contains the detail group 214 Composite lowerRightBase = new Composite(rightSash, SWT.BORDER); 215 lowerRightBase.setLayout(new FillLayout()); 216 Group grpNodeDetail = new Group(lowerRightBase, SWT.NONE); 217 grpNodeDetail.setLayout(new FillLayout(SWT.HORIZONTAL)); 218 grpNodeDetail.setText("Node Detail"); 219 220 Composite tableContainer = new Composite(grpNodeDetail, SWT.NONE); 221 222 TableColumnLayout columnLayout = new TableColumnLayout(); 223 tableContainer.setLayout(columnLayout); 224 225 mTableViewer = new TableViewer(tableContainer, SWT.NONE | SWT.FULL_SELECTION); 226 Table table = mTableViewer.getTable(); 227 table.setLinesVisible(true); 228 // use ArrayContentProvider here, it assumes the input to the TableViewer 229 // is an array, where each element represents a row in the table 230 mTableViewer.setContentProvider(new ArrayContentProvider()); 231 232 TableViewerColumn tableViewerColumnKey = new TableViewerColumn(mTableViewer, SWT.NONE); 233 TableColumn tblclmnKey = tableViewerColumnKey.getColumn(); 234 tableViewerColumnKey.setLabelProvider(new ColumnLabelProvider() { 235 @Override 236 public String getText(Object element) { 237 if (element instanceof AttributePair) { 238 // first column, shows the attribute name 239 return ((AttributePair)element).key; 240 } 241 return super.getText(element); 242 } 243 }); 244 columnLayout.setColumnData(tblclmnKey, 245 new ColumnWeightData(1, ColumnWeightData.MINIMUM_WIDTH, true)); 246 247 TableViewerColumn tableViewerColumnValue = new TableViewerColumn(mTableViewer, SWT.NONE); 248 tableViewerColumnValue.setEditingSupport(new AttributeTableEditingSupport(mTableViewer)); 249 TableColumn tblclmnValue = tableViewerColumnValue.getColumn(); 250 columnLayout.setColumnData(tblclmnValue, 251 new ColumnWeightData(2, ColumnWeightData.MINIMUM_WIDTH, true)); 252 tableViewerColumnValue.setLabelProvider(new ColumnLabelProvider() { 253 @Override 254 public String getText(Object element) { 255 if (element instanceof AttributePair) { 256 // second column, shows the attribute value 257 return ((AttributePair)element).value; 258 } 259 return super.getText(element); 260 } 261 }); 262 // sets the ratio of the vertical split: left 5 vs right 3 263 baseSash.setWeights(new int[]{5, 3}); 264 return baseSash; 265 } 266 267 /** 268 * Create the actions. 269 */ 270 private void createActions() { 271 mOpenFilesAction = new OpenFilesAction(this); 272 mExpandAllAction = new ExpandAllAction(this); 273 mScreenshotAction = new ScreenshotAction(this); 274 mToggleNafAction = new ToggleNafAction(); 275 } 276 277 /** 278 * Launch the application. 279 * 280 * @param args 281 */ 282 public static void main(String args[]) { 283 try { 284 UiAutomatorViewer window = new UiAutomatorViewer(); 285 window.setBlockOnOpen(true); 286 window.open(); 287 UiAutomatorModel.getModel().cleanUp(); 288 } catch (Exception e) { 289 e.printStackTrace(); 290 } 291 } 292 293 /** 294 * Configure the shell. 295 * 296 * @param newShell 297 */ 298 @Override 299 protected void configureShell(Shell newShell) { 300 super.configureShell(newShell); 301 newShell.setText("UI Automator Viewer"); 302 } 303 304 305 /** 306 * Asks the Model for screenshot and xml tree data, then populates the screenshot 307 * area and tree view accordingly 308 */ 309 public void loadScreenshotAndXml() { 310 mScreenshotCanvas.redraw(); 311 // load xml into tree 312 BasicTreeNode wrapper = new BasicTreeNode(); 313 // putting another root node on top of existing root node 314 // because Tree seems to like to hide the root node 315 wrapper.addChild(UiAutomatorModel.getModel().getXmlRootNode()); 316 mTreeViewer.setInput(wrapper); 317 mTreeViewer.getTree().setFocus(); 318 } 319 320 /* 321 * Causes a redraw of the canvas. 322 * 323 * The drawing code of canvas will handle highlighted nodes and etc based on data 324 * retrieved from Model 325 */ 326 public void updateScreenshot() { 327 mScreenshotCanvas.redraw(); 328 } 329 330 public void expandAll() { 331 mTreeViewer.expandAll(); 332 } 333 334 public void updateTreeSelection(BasicTreeNode node) { 335 mTreeViewer.setSelection(new StructuredSelection(node), true); 336 } 337 338 public void loadAttributeTable() { 339 // udpate the lower right corner table to show the attributes of the node 340 mTableViewer.setInput( 341 UiAutomatorModel.getModel().getSelectedNode().getAttributesArray()); 342 } 343 344 @Override 345 protected Point getInitialSize() { 346 return new Point(800, 600); 347 } 348 349 private void updateScreenshotTransformation() { 350 Rectangle canvas = mScreenshotCanvas.getBounds(); 351 Rectangle image = UiAutomatorModel.getModel().getScreenshot().getBounds(); 352 float scaleX = (canvas.width - 2 * IMG_BORDER - 1) / (float)image.width; 353 float scaleY = (canvas.height - 2 * IMG_BORDER - 1) / (float)image.height; 354 // use the smaller scale here so that we can fit the entire screenshot 355 mScale = Math.min(scaleX, scaleY); 356 // calculate translation values to center the image on the canvas 357 mDx = (canvas.width - getScaledSize(image.width) - IMG_BORDER * 2) / 2 + IMG_BORDER; 358 mDy = (canvas.height - getScaledSize(image.height) - IMG_BORDER * 2) / 2 + IMG_BORDER; 359 } 360 361 private int getScaledSize(int size) { 362 if (mScale == 1.0f) { 363 return size; 364 } else { 365 return new Double(Math.floor((size * mScale))).intValue(); 366 } 367 } 368 369 private int getInverseScaledSize(int size) { 370 if (mScale == 1.0f) { 371 return size; 372 } else { 373 return new Double(Math.floor((size / mScale))).intValue(); 374 } 375 } 376 377 private class AttributeTableEditingSupport extends EditingSupport { 378 379 private TableViewer mViewer; 380 381 public AttributeTableEditingSupport(TableViewer viewer) { 382 super(viewer); 383 mViewer = viewer; 384 } 385 386 @Override 387 protected boolean canEdit(Object arg0) { 388 return true; 389 } 390 391 @Override 392 protected CellEditor getCellEditor(Object arg0) { 393 return new TextCellEditor(mViewer.getTable()); 394 } 395 396 @Override 397 protected Object getValue(Object o) { 398 return ((AttributePair)o).value; 399 } 400 401 @Override 402 protected void setValue(Object arg0, Object arg1) { 403 } 404 405 } 406 } 407