Home | History | Annotate | Download | only in uiautomator
      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