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.ToggleNafAction; 21 import com.android.uiautomator.tree.AttributePair; 22 import com.android.uiautomator.tree.BasicTreeNode; 23 import com.android.uiautomator.tree.BasicTreeNodeContentProvider; 24 25 import org.eclipse.jface.action.ToolBarManager; 26 import org.eclipse.jface.layout.TableColumnLayout; 27 import org.eclipse.jface.viewers.ArrayContentProvider; 28 import org.eclipse.jface.viewers.CellEditor; 29 import org.eclipse.jface.viewers.ColumnLabelProvider; 30 import org.eclipse.jface.viewers.ColumnWeightData; 31 import org.eclipse.jface.viewers.EditingSupport; 32 import org.eclipse.jface.viewers.ISelectionChangedListener; 33 import org.eclipse.jface.viewers.IStructuredSelection; 34 import org.eclipse.jface.viewers.LabelProvider; 35 import org.eclipse.jface.viewers.SelectionChangedEvent; 36 import org.eclipse.jface.viewers.StructuredSelection; 37 import org.eclipse.jface.viewers.TableViewer; 38 import org.eclipse.jface.viewers.TableViewerColumn; 39 import org.eclipse.jface.viewers.TextCellEditor; 40 import org.eclipse.jface.viewers.TreeViewer; 41 import org.eclipse.swt.SWT; 42 import org.eclipse.swt.custom.SashForm; 43 import org.eclipse.swt.custom.StackLayout; 44 import org.eclipse.swt.events.MouseAdapter; 45 import org.eclipse.swt.events.MouseEvent; 46 import org.eclipse.swt.events.MouseMoveListener; 47 import org.eclipse.swt.events.PaintEvent; 48 import org.eclipse.swt.events.PaintListener; 49 import org.eclipse.swt.events.SelectionAdapter; 50 import org.eclipse.swt.events.SelectionEvent; 51 import org.eclipse.swt.graphics.Image; 52 import org.eclipse.swt.graphics.ImageData; 53 import org.eclipse.swt.graphics.ImageLoader; 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.Button; 60 import org.eclipse.swt.widgets.Canvas; 61 import org.eclipse.swt.widgets.Composite; 62 import org.eclipse.swt.widgets.Display; 63 import org.eclipse.swt.widgets.FileDialog; 64 import org.eclipse.swt.widgets.Group; 65 import org.eclipse.swt.widgets.Table; 66 import org.eclipse.swt.widgets.TableColumn; 67 import org.eclipse.swt.widgets.Tree; 68 69 import java.io.File; 70 71 public class UiAutomatorView extends Composite { 72 private static final int IMG_BORDER = 2; 73 74 // The screenshot area is made of a stack layout of two components: screenshot canvas and 75 // a "specify screenshot" button. If a screenshot is already available, then that is displayed 76 // on the canvas. If it is not availble, then the "specify screenshot" button is displayed. 77 private Composite mScreenshotComposite; 78 private StackLayout mStackLayout; 79 private Composite mSetScreenshotComposite; 80 private Canvas mScreenshotCanvas; 81 82 private TreeViewer mTreeViewer; 83 private TableViewer mTableViewer; 84 85 private float mScale = 1.0f; 86 private int mDx, mDy; 87 88 private UiAutomatorModel mModel; 89 private File mModelFile; 90 private Image mScreenshot; 91 92 public UiAutomatorView(Composite parent, int style) { 93 super(parent, SWT.NONE); 94 setLayout(new FillLayout()); 95 96 SashForm baseSash = new SashForm(this, SWT.HORIZONTAL); 97 98 mScreenshotComposite = new Composite(baseSash, SWT.BORDER); 99 mStackLayout = new StackLayout(); 100 mScreenshotComposite.setLayout(mStackLayout); 101 102 // draw the canvas with border, so the divider area for sash form can be highlighted 103 mScreenshotCanvas = new Canvas(mScreenshotComposite, SWT.BORDER); 104 mStackLayout.topControl = mScreenshotCanvas; 105 mScreenshotComposite.layout(); 106 107 mScreenshotCanvas.addMouseListener(new MouseAdapter() { 108 @Override 109 public void mouseUp(MouseEvent e) { 110 if (mModel != null) { 111 mModel.toggleExploreMode(); 112 redrawScreenshot(); 113 } 114 } 115 }); 116 mScreenshotCanvas.setBackground( 117 getShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND)); 118 mScreenshotCanvas.addPaintListener(new PaintListener() { 119 @Override 120 public void paintControl(PaintEvent e) { 121 if (mScreenshot != null) { 122 updateScreenshotTransformation(); 123 // shifting the image here, so that there's a border around screen shot 124 // this makes highlighting red rectangles on the screen shot edges more visible 125 Transform t = new Transform(e.gc.getDevice()); 126 t.translate(mDx, mDy); 127 t.scale(mScale, mScale); 128 e.gc.setTransform(t); 129 e.gc.drawImage(mScreenshot, 0, 0); 130 // this resets the transformation to identity transform, i.e. no change 131 // we don't use transformation here because it will cause the line pattern 132 // and line width of highlight rect to be scaled, causing to appear to be blurry 133 e.gc.setTransform(null); 134 if (mModel.shouldShowNafNodes()) { 135 // highlight the "Not Accessibility Friendly" nodes 136 e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW)); 137 e.gc.setBackground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW)); 138 for (Rectangle r : mModel.getNafNodes()) { 139 e.gc.setAlpha(50); 140 e.gc.fillRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y), 141 getScaledSize(r.width), getScaledSize(r.height)); 142 e.gc.setAlpha(255); 143 e.gc.setLineStyle(SWT.LINE_SOLID); 144 e.gc.setLineWidth(2); 145 e.gc.drawRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y), 146 getScaledSize(r.width), getScaledSize(r.height)); 147 } 148 } 149 // draw the mouseover rects 150 Rectangle rect = mModel.getCurrentDrawingRect(); 151 if (rect != null) { 152 e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_RED)); 153 if (mModel.isExploreMode()) { 154 // when we highlight nodes dynamically on mouse move, 155 // use dashed borders 156 e.gc.setLineStyle(SWT.LINE_DASH); 157 e.gc.setLineWidth(1); 158 } else { 159 // when highlighting nodes on tree node selection, 160 // use solid borders 161 e.gc.setLineStyle(SWT.LINE_SOLID); 162 e.gc.setLineWidth(2); 163 } 164 e.gc.drawRectangle(mDx + getScaledSize(rect.x), mDy + getScaledSize(rect.y), 165 getScaledSize(rect.width), getScaledSize(rect.height)); 166 } 167 } 168 } 169 }); 170 mScreenshotCanvas.addMouseMoveListener(new MouseMoveListener() { 171 @Override 172 public void mouseMove(MouseEvent e) { 173 if (mModel != null && mModel.isExploreMode()) { 174 BasicTreeNode node = mModel.updateSelectionForCoordinates( 175 getInverseScaledSize(e.x - mDx), 176 getInverseScaledSize(e.y - mDy)); 177 if (node != null) { 178 updateTreeSelection(node); 179 } 180 } 181 } 182 }); 183 184 mSetScreenshotComposite = new Composite(mScreenshotComposite, SWT.NONE); 185 mSetScreenshotComposite.setLayout(new GridLayout()); 186 187 final Button setScreenshotButton = new Button(mSetScreenshotComposite, SWT.PUSH); 188 setScreenshotButton.setText("Specify Screenshot..."); 189 setScreenshotButton.addSelectionListener(new SelectionAdapter() { 190 @Override 191 public void widgetSelected(SelectionEvent arg0) { 192 FileDialog fd = new FileDialog(setScreenshotButton.getShell()); 193 fd.setFilterExtensions(new String[] { "*.png" }); 194 if (mModelFile != null) { 195 fd.setFilterPath(mModelFile.getParent()); 196 } 197 String screenshotPath = fd.open(); 198 if (screenshotPath == null) { 199 return; 200 } 201 202 ImageData[] data; 203 try { 204 data = new ImageLoader().load(screenshotPath); 205 } catch (Exception e) { 206 return; 207 } 208 209 // "data" is an array, probably used to handle images that has multiple frames 210 // i.e. gifs or icons, we just care if it has at least one here 211 if (data.length < 1) { 212 return; 213 } 214 215 mScreenshot = new Image(Display.getDefault(), data[0]); 216 redrawScreenshot(); 217 } 218 }); 219 220 221 // right sash is split into 2 parts: upper-right and lower-right 222 // both are composites with borders, so that the horizontal divider can be highlighted by 223 // the borders 224 SashForm rightSash = new SashForm(baseSash, SWT.VERTICAL); 225 226 // upper-right base contains the toolbar and the tree 227 Composite upperRightBase = new Composite(rightSash, SWT.BORDER); 228 upperRightBase.setLayout(new GridLayout(1, false)); 229 230 ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT); 231 toolBarManager.add(new ExpandAllAction(this)); 232 toolBarManager.add(new ToggleNafAction(this)); 233 toolBarManager.createControl(upperRightBase); 234 235 mTreeViewer = new TreeViewer(upperRightBase, SWT.NONE); 236 mTreeViewer.setContentProvider(new BasicTreeNodeContentProvider()); 237 // default LabelProvider uses toString() to generate text to display 238 mTreeViewer.setLabelProvider(new LabelProvider()); 239 mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() { 240 @Override 241 public void selectionChanged(SelectionChangedEvent event) { 242 BasicTreeNode selectedNode = null; 243 if (event.getSelection() instanceof IStructuredSelection) { 244 IStructuredSelection selection = (IStructuredSelection) event.getSelection(); 245 Object o = selection.getFirstElement(); 246 if (o instanceof BasicTreeNode) { 247 selectedNode = (BasicTreeNode) o; 248 } 249 } 250 251 mModel.setSelectedNode(selectedNode); 252 redrawScreenshot(); 253 if (selectedNode != null) { 254 loadAttributeTable(); 255 } 256 } 257 }); 258 Tree tree = mTreeViewer.getTree(); 259 tree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); 260 // move focus so that it's not on tool bar (looks weird) 261 tree.setFocus(); 262 263 // lower-right base contains the detail group 264 Composite lowerRightBase = new Composite(rightSash, SWT.BORDER); 265 lowerRightBase.setLayout(new FillLayout()); 266 Group grpNodeDetail = new Group(lowerRightBase, SWT.NONE); 267 grpNodeDetail.setLayout(new FillLayout(SWT.HORIZONTAL)); 268 grpNodeDetail.setText("Node Detail"); 269 270 Composite tableContainer = new Composite(grpNodeDetail, SWT.NONE); 271 272 TableColumnLayout columnLayout = new TableColumnLayout(); 273 tableContainer.setLayout(columnLayout); 274 275 mTableViewer = new TableViewer(tableContainer, SWT.NONE | SWT.FULL_SELECTION); 276 Table table = mTableViewer.getTable(); 277 table.setLinesVisible(true); 278 // use ArrayContentProvider here, it assumes the input to the TableViewer 279 // is an array, where each element represents a row in the table 280 mTableViewer.setContentProvider(new ArrayContentProvider()); 281 282 TableViewerColumn tableViewerColumnKey = new TableViewerColumn(mTableViewer, SWT.NONE); 283 TableColumn tblclmnKey = tableViewerColumnKey.getColumn(); 284 tableViewerColumnKey.setLabelProvider(new ColumnLabelProvider() { 285 @Override 286 public String getText(Object element) { 287 if (element instanceof AttributePair) { 288 // first column, shows the attribute name 289 return ((AttributePair)element).key; 290 } 291 return super.getText(element); 292 } 293 }); 294 columnLayout.setColumnData(tblclmnKey, 295 new ColumnWeightData(1, ColumnWeightData.MINIMUM_WIDTH, true)); 296 297 TableViewerColumn tableViewerColumnValue = new TableViewerColumn(mTableViewer, SWT.NONE); 298 tableViewerColumnValue.setEditingSupport(new AttributeTableEditingSupport(mTableViewer)); 299 TableColumn tblclmnValue = tableViewerColumnValue.getColumn(); 300 columnLayout.setColumnData(tblclmnValue, 301 new ColumnWeightData(2, ColumnWeightData.MINIMUM_WIDTH, true)); 302 tableViewerColumnValue.setLabelProvider(new ColumnLabelProvider() { 303 @Override 304 public String getText(Object element) { 305 if (element instanceof AttributePair) { 306 // second column, shows the attribute value 307 return ((AttributePair)element).value; 308 } 309 return super.getText(element); 310 } 311 }); 312 // sets the ratio of the vertical split: left 5 vs right 3 313 baseSash.setWeights(new int[]{5, 3}); 314 } 315 316 private int getScaledSize(int size) { 317 if (mScale == 1.0f) { 318 return size; 319 } else { 320 return new Double(Math.floor((size * mScale))).intValue(); 321 } 322 } 323 324 private int getInverseScaledSize(int size) { 325 if (mScale == 1.0f) { 326 return size; 327 } else { 328 return new Double(Math.floor((size / mScale))).intValue(); 329 } 330 } 331 332 private void updateScreenshotTransformation() { 333 Rectangle canvas = mScreenshotCanvas.getBounds(); 334 Rectangle image = mScreenshot.getBounds(); 335 float scaleX = (canvas.width - 2 * IMG_BORDER - 1) / (float)image.width; 336 float scaleY = (canvas.height - 2 * IMG_BORDER - 1) / (float)image.height; 337 // use the smaller scale here so that we can fit the entire screenshot 338 mScale = Math.min(scaleX, scaleY); 339 // calculate translation values to center the image on the canvas 340 mDx = (canvas.width - getScaledSize(image.width) - IMG_BORDER * 2) / 2 + IMG_BORDER; 341 mDy = (canvas.height - getScaledSize(image.height) - IMG_BORDER * 2) / 2 + IMG_BORDER; 342 } 343 344 private class AttributeTableEditingSupport extends EditingSupport { 345 346 private TableViewer mViewer; 347 348 public AttributeTableEditingSupport(TableViewer viewer) { 349 super(viewer); 350 mViewer = viewer; 351 } 352 353 @Override 354 protected boolean canEdit(Object arg0) { 355 return true; 356 } 357 358 @Override 359 protected CellEditor getCellEditor(Object arg0) { 360 return new TextCellEditor(mViewer.getTable()); 361 } 362 363 @Override 364 protected Object getValue(Object o) { 365 return ((AttributePair)o).value; 366 } 367 368 @Override 369 protected void setValue(Object arg0, Object arg1) { 370 } 371 } 372 373 /** 374 * Causes a redraw of the canvas. 375 * 376 * The drawing code of canvas will handle highlighted nodes and etc based on data 377 * retrieved from Model 378 */ 379 public void redrawScreenshot() { 380 if (mScreenshot == null) { 381 mStackLayout.topControl = mSetScreenshotComposite; 382 } else { 383 mStackLayout.topControl = mScreenshotCanvas; 384 } 385 mScreenshotComposite.layout(); 386 387 mScreenshotCanvas.redraw(); 388 } 389 390 public void setInputHierarchy(Object input) { 391 mTreeViewer.setInput(input); 392 } 393 394 public void loadAttributeTable() { 395 // udpate the lower right corner table to show the attributes of the node 396 mTableViewer.setInput(mModel.getSelectedNode().getAttributesArray()); 397 } 398 399 public void expandAll() { 400 mTreeViewer.expandAll(); 401 } 402 403 public void updateTreeSelection(BasicTreeNode node) { 404 mTreeViewer.setSelection(new StructuredSelection(node), true); 405 } 406 407 public void setModel(UiAutomatorModel model, File modelBackingFile, Image screenshot) { 408 mModel = model; 409 mModelFile = modelBackingFile; 410 411 if (mScreenshot != null) { 412 mScreenshot.dispose(); 413 } 414 mScreenshot = screenshot; 415 416 redrawScreenshot(); 417 // load xml into tree 418 BasicTreeNode wrapper = new BasicTreeNode(); 419 // putting another root node on top of existing root node 420 // because Tree seems to like to hide the root node 421 wrapper.addChild(mModel.getXmlRootNode()); 422 setInputHierarchy(wrapper); 423 mTreeViewer.getTree().setFocus(); 424 425 } 426 427 public boolean shouldShowNafNodes() { 428 return mModel != null ? mModel.shouldShowNafNodes() : false; 429 } 430 431 public void toggleShowNaf() { 432 if (mModel != null) { 433 mModel.toggleShowNaf(); 434 } 435 } 436 } 437