Home | History | Annotate | Download | only in gle2
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
      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.ide.eclipse.adt.internal.editors.layout.gle2;
     18 
     19 import static com.android.SdkConstants.ANDROID_URI;
     20 import static com.android.SdkConstants.ATTR_COLUMN_COUNT;
     21 import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
     22 import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
     23 import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
     24 import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
     25 import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
     26 import static com.android.SdkConstants.ATTR_ROW_COUNT;
     27 import static com.android.SdkConstants.ATTR_SRC;
     28 import static com.android.SdkConstants.ATTR_TEXT;
     29 import static com.android.SdkConstants.AUTO_URI;
     30 import static com.android.SdkConstants.DRAWABLE_PREFIX;
     31 import static com.android.SdkConstants.GRID_LAYOUT;
     32 import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
     33 import static com.android.SdkConstants.URI_PREFIX;
     34 import static org.eclipse.jface.viewers.StyledString.COUNTER_STYLER;
     35 import static org.eclipse.jface.viewers.StyledString.QUALIFIER_STYLER;
     36 
     37 import com.android.SdkConstants;
     38 import com.android.annotations.VisibleForTesting;
     39 import com.android.ide.common.api.INode;
     40 import com.android.ide.common.api.InsertType;
     41 import com.android.ide.common.layout.BaseLayoutRule;
     42 import com.android.ide.common.layout.GridLayoutRule;
     43 import com.android.ide.eclipse.adt.AdtPlugin;
     44 import com.android.ide.eclipse.adt.AdtUtils;
     45 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
     46 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
     47 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
     48 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
     49 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
     50 import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertySheetPage;
     51 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
     52 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
     53 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     54 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
     55 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     56 import com.android.utils.Pair;
     57 
     58 import org.eclipse.core.resources.IMarker;
     59 import org.eclipse.core.resources.IProject;
     60 import org.eclipse.jface.action.Action;
     61 import org.eclipse.jface.action.ActionContributionItem;
     62 import org.eclipse.jface.action.IAction;
     63 import org.eclipse.jface.action.IContributionItem;
     64 import org.eclipse.jface.action.IMenuListener;
     65 import org.eclipse.jface.action.IMenuManager;
     66 import org.eclipse.jface.action.IToolBarManager;
     67 import org.eclipse.jface.action.MenuManager;
     68 import org.eclipse.jface.action.Separator;
     69 import org.eclipse.jface.preference.JFacePreferences;
     70 import org.eclipse.jface.viewers.DoubleClickEvent;
     71 import org.eclipse.jface.viewers.IDoubleClickListener;
     72 import org.eclipse.jface.viewers.IElementComparer;
     73 import org.eclipse.jface.viewers.ISelection;
     74 import org.eclipse.jface.viewers.ITreeContentProvider;
     75 import org.eclipse.jface.viewers.ITreeSelection;
     76 import org.eclipse.jface.viewers.SelectionChangedEvent;
     77 import org.eclipse.jface.viewers.StyledCellLabelProvider;
     78 import org.eclipse.jface.viewers.StyledString;
     79 import org.eclipse.jface.viewers.StyledString.Styler;
     80 import org.eclipse.jface.viewers.TreePath;
     81 import org.eclipse.jface.viewers.TreeSelection;
     82 import org.eclipse.jface.viewers.TreeViewer;
     83 import org.eclipse.jface.viewers.Viewer;
     84 import org.eclipse.jface.viewers.ViewerCell;
     85 import org.eclipse.swt.SWT;
     86 import org.eclipse.swt.dnd.DND;
     87 import org.eclipse.swt.dnd.Transfer;
     88 import org.eclipse.swt.events.DisposeEvent;
     89 import org.eclipse.swt.events.DisposeListener;
     90 import org.eclipse.swt.events.KeyEvent;
     91 import org.eclipse.swt.events.KeyListener;
     92 import org.eclipse.swt.events.MenuDetectEvent;
     93 import org.eclipse.swt.events.MenuDetectListener;
     94 import org.eclipse.swt.events.MouseEvent;
     95 import org.eclipse.swt.events.MouseListener;
     96 import org.eclipse.swt.graphics.Color;
     97 import org.eclipse.swt.graphics.Image;
     98 import org.eclipse.swt.graphics.Point;
     99 import org.eclipse.swt.graphics.Rectangle;
    100 import org.eclipse.swt.layout.FillLayout;
    101 import org.eclipse.swt.widgets.Composite;
    102 import org.eclipse.swt.widgets.Control;
    103 import org.eclipse.swt.widgets.Display;
    104 import org.eclipse.swt.widgets.Event;
    105 import org.eclipse.swt.widgets.Label;
    106 import org.eclipse.swt.widgets.Listener;
    107 import org.eclipse.swt.widgets.Shell;
    108 import org.eclipse.swt.widgets.Text;
    109 import org.eclipse.swt.widgets.Tree;
    110 import org.eclipse.swt.widgets.TreeItem;
    111 import org.eclipse.ui.IActionBars;
    112 import org.eclipse.ui.IEditorPart;
    113 import org.eclipse.ui.INullSelectionListener;
    114 import org.eclipse.ui.IWorkbenchPart;
    115 import org.eclipse.ui.actions.ActionFactory;
    116 import org.eclipse.ui.views.contentoutline.ContentOutlinePage;
    117 import org.eclipse.wb.core.controls.SelfOrientingSashForm;
    118 import org.eclipse.wb.internal.core.editor.structure.IPage;
    119 import org.eclipse.wb.internal.core.editor.structure.PageSiteComposite;
    120 import org.w3c.dom.Element;
    121 import org.w3c.dom.Node;
    122 
    123 import java.util.ArrayList;
    124 import java.util.HashSet;
    125 import java.util.List;
    126 import java.util.Set;
    127 
    128 /**
    129  * An outline page for the layout canvas view.
    130  * <p/>
    131  * The page is created by {@link LayoutEditorDelegate#delegateGetAdapter(Class)}. This means
    132  * we have *one* instance of the outline page per open canvas editor.
    133  * <p/>
    134  * It sets itself as a listener on the site's selection service in order to be
    135  * notified of the canvas' selection changes.
    136  * The underlying page is also a selection provider (via IContentOutlinePage)
    137  * and as such it will broadcast selection changes to the site's selection service
    138  * (on which both the layout editor part and the property sheet page listen.)
    139  */
    140 public class OutlinePage extends ContentOutlinePage
    141     implements INullSelectionListener, IPage {
    142 
    143     /** Label which separates outline text from additional attributes like text prefix or url */
    144     private static final String LABEL_SEPARATOR = " - ";
    145 
    146     /** Max character count in labels, used for truncation */
    147     private static final int LABEL_MAX_WIDTH = 50;
    148 
    149     /**
    150      * The graphical editor that created this outline.
    151      */
    152     private final GraphicalEditorPart mGraphicalEditorPart;
    153 
    154     /**
    155      * RootWrapper is a workaround: we can't set the input of the TreeView to its root
    156      * element, so we introduce a fake parent.
    157      */
    158     private final RootWrapper mRootWrapper = new RootWrapper();
    159 
    160     /**
    161      * Menu manager for the context menu actions.
    162      * The actions delegate to the current GraphicalEditorPart.
    163      */
    164     private MenuManager mMenuManager;
    165 
    166     private Composite mControl;
    167     private PropertySheetPage mPropertySheet;
    168     private PageSiteComposite mPropertySheetComposite;
    169     private boolean mShowPropertySheet;
    170     private boolean mShowHeader;
    171     private boolean mIgnoreSelection;
    172     private boolean mActive = true;
    173 
    174     /** Action to Select All in the tree */
    175     private final Action mTreeSelectAllAction = new Action() {
    176         @Override
    177         public void run() {
    178             getTreeViewer().getTree().selectAll();
    179             OutlinePage.this.fireSelectionChanged(getSelection());
    180         }
    181 
    182         @Override
    183         public String getId() {
    184             return ActionFactory.SELECT_ALL.getId();
    185         }
    186     };
    187 
    188     /** Action for moving items up in the tree */
    189     private Action mMoveUpAction = new Action("Move Up\t-",
    190             IconFactory.getInstance().getImageDescriptor("up")) { //$NON-NLS-1$
    191 
    192         @Override
    193         public String getId() {
    194             return "adt.outline.moveup"; //$NON-NLS-1$
    195         }
    196 
    197         @Override
    198         public boolean isEnabled() {
    199             return canMove(false);
    200         }
    201 
    202         @Override
    203         public void run() {
    204             move(false);
    205         }
    206     };
    207 
    208     /** Action for moving items down in the tree */
    209     private Action mMoveDownAction = new Action("Move Down\t+",
    210             IconFactory.getInstance().getImageDescriptor("down")) { //$NON-NLS-1$
    211 
    212         @Override
    213         public String getId() {
    214             return "adt.outline.movedown"; //$NON-NLS-1$
    215         }
    216 
    217         @Override
    218         public boolean isEnabled() {
    219             return canMove(true);
    220         }
    221 
    222         @Override
    223         public void run() {
    224             move(true);
    225         }
    226     };
    227 
    228     /**
    229      * Creates a new {@link OutlinePage} associated with the given editor
    230      *
    231      * @param graphicalEditorPart the editor associated with this outline
    232      */
    233     public OutlinePage(GraphicalEditorPart graphicalEditorPart) {
    234         super();
    235         mGraphicalEditorPart = graphicalEditorPart;
    236     }
    237 
    238     @Override
    239     public Control getControl() {
    240         // We've injected some controls between the root of the outline page
    241         // and the tree control, so return the actual root (a sash form) rather
    242         // than the superclass' implementation which returns the tree. If we don't
    243         // do this, various checks in the outline page which checks that getControl().getParent()
    244         // is the outline window itself will ignore this page.
    245         return mControl;
    246     }
    247 
    248     void setActive(boolean active) {
    249         if (active != mActive) {
    250             mActive = active;
    251 
    252             // Outlines are by default active when they are created; this is intended
    253             // for deactivating a hidden outline and later reactivating it
    254             assert mControl != null;
    255             if (active) {
    256                 getSite().getPage().addSelectionListener(this);
    257                 setModel(mGraphicalEditorPart.getCanvasControl().getViewHierarchy().getRoot());
    258             } else {
    259                 getSite().getPage().removeSelectionListener(this);
    260                 mRootWrapper.setRoot(null);
    261                 if (mPropertySheet != null) {
    262                     mPropertySheet.selectionChanged(null, TreeSelection.EMPTY);
    263                 }
    264             }
    265         }
    266     }
    267 
    268     /** Refresh all the icon state */
    269     public void refreshIcons() {
    270         TreeViewer treeViewer = getTreeViewer();
    271         if (treeViewer != null) {
    272             Tree tree = treeViewer.getTree();
    273             if (tree != null && !tree.isDisposed()) {
    274                 treeViewer.refresh();
    275             }
    276         }
    277     }
    278 
    279     /**
    280      * Set whether the outline should be shown in the header
    281      *
    282      * @param show whether a header should be shown
    283      */
    284     public void setShowHeader(boolean show) {
    285         mShowHeader = show;
    286     }
    287 
    288     /**
    289      * Set whether the property sheet should be shown within this outline
    290      *
    291      * @param show whether the property sheet should show
    292      */
    293     public void setShowPropertySheet(boolean show) {
    294         if (show != mShowPropertySheet) {
    295             mShowPropertySheet = show;
    296             if (mControl == null) {
    297                 return;
    298             }
    299 
    300             if (show && mPropertySheet == null) {
    301                 createPropertySheet();
    302             } else if (!show) {
    303                 mPropertySheetComposite.dispose();
    304                 mPropertySheetComposite = null;
    305                 mPropertySheet.dispose();
    306                 mPropertySheet = null;
    307             }
    308 
    309             mControl.layout();
    310         }
    311     }
    312 
    313     @Override
    314     public void createControl(Composite parent) {
    315         mControl = new SelfOrientingSashForm(parent, SWT.VERTICAL);
    316 
    317         if (mShowHeader) {
    318             PageSiteComposite mOutlineComposite = new PageSiteComposite(mControl, SWT.BORDER);
    319             mOutlineComposite.setTitleText("Outline");
    320             mOutlineComposite.setTitleImage(IconFactory.getInstance().getIcon("components_view"));
    321             mOutlineComposite.setPage(new IPage() {
    322                 @Override
    323                 public void createControl(Composite outlineParent) {
    324                     createOutline(outlineParent);
    325                 }
    326 
    327                 @Override
    328                 public void dispose() {
    329                 }
    330 
    331                 @Override
    332                 public Control getControl() {
    333                     return getTreeViewer().getTree();
    334                 }
    335 
    336                 @Override
    337                 public void setToolBar(IToolBarManager toolBarManager) {
    338                     makeContributions(null, toolBarManager, null);
    339                     toolBarManager.update(false);
    340                 }
    341 
    342                 @Override
    343                 public void setFocus() {
    344                     getControl().setFocus();
    345                 }
    346             });
    347         } else {
    348             createOutline(mControl);
    349         }
    350 
    351         if (mShowPropertySheet) {
    352             createPropertySheet();
    353         }
    354     }
    355 
    356     private void createOutline(Composite parent) {
    357         if (AdtUtils.isEclipse4()) {
    358             // This is a workaround for the focus behavior in Eclipse 4 where
    359             // the framework ends up calling setFocus() on the first widget in the outline
    360             // AFTER a mouse click has been received. Specifically, if the user clicks in
    361             // the embedded property sheet to for example give a Text property editor focus,
    362             // then after the mouse click, the Outline window activation event is processed,
    363             // and this event causes setFocus() to be called first on the PageBookView (which
    364             // ends up calling setFocus on the first control, normally the TreeViewer), and
    365             // then on the Page itself. We're dealing with the page setFocus() in the override
    366             // of that method in the class, such that it does nothing.
    367             // However, we have to also disable the setFocus on the first control in the
    368             // outline page. To deal with that, we create our *own* first control in the
    369             // outline, and make its setFocus() a no-op. We also make it invisible, since we
    370             // don't actually want anything but the tree viewer showing in the outline.
    371             Text text = new Text(parent, SWT.NONE) {
    372                 @Override
    373                 public boolean setFocus() {
    374                     // Focus no-op
    375                     return true;
    376                 }
    377 
    378                 @Override
    379                 protected void checkSubclass() {
    380                     // Disable the check that prevents subclassing of SWT components
    381                 }
    382             };
    383             text.setVisible(false);
    384         }
    385 
    386         super.createControl(parent);
    387 
    388         TreeViewer tv = getTreeViewer();
    389         tv.setAutoExpandLevel(2);
    390         tv.setContentProvider(new ContentProvider());
    391         tv.setLabelProvider(new LabelProvider());
    392         tv.setInput(mRootWrapper);
    393         tv.expandToLevel(mRootWrapper.getRoot(), 2);
    394 
    395         int supportedOperations = DND.DROP_COPY | DND.DROP_MOVE;
    396         Transfer[] transfers = new Transfer[] {
    397             SimpleXmlTransfer.getInstance()
    398         };
    399 
    400         tv.addDropSupport(supportedOperations, transfers, new OutlineDropListener(this, tv));
    401         tv.addDragSupport(supportedOperations, transfers, new OutlineDragListener(this, tv));
    402 
    403         // The tree viewer will hold CanvasViewInfo instances, however these
    404         // change each time the canvas is reloaded. OTOH layoutlib gives us
    405         // constant UiView keys which we can use to perform tree item comparisons.
    406         tv.setComparer(new IElementComparer() {
    407             @Override
    408             public int hashCode(Object element) {
    409                 if (element instanceof CanvasViewInfo) {
    410                     UiViewElementNode key = ((CanvasViewInfo) element).getUiViewNode();
    411                     if (key != null) {
    412                         return key.hashCode();
    413                     }
    414                 }
    415                 if (element != null) {
    416                     return element.hashCode();
    417                 }
    418                 return 0;
    419             }
    420 
    421             @Override
    422             public boolean equals(Object a, Object b) {
    423                 if (a instanceof CanvasViewInfo && b instanceof CanvasViewInfo) {
    424                     UiViewElementNode keyA = ((CanvasViewInfo) a).getUiViewNode();
    425                     UiViewElementNode keyB = ((CanvasViewInfo) b).getUiViewNode();
    426                     if (keyA != null) {
    427                         return keyA.equals(keyB);
    428                     }
    429                 }
    430                 if (a != null) {
    431                     return a.equals(b);
    432                 }
    433                 return false;
    434             }
    435         });
    436         tv.addDoubleClickListener(new IDoubleClickListener() {
    437             @Override
    438             public void doubleClick(DoubleClickEvent event) {
    439                 // This used to open the property view, but now that properties are docked
    440                 // let's use it for something else -- such as showing the editor source
    441                 /*
    442                 // Front properties panel; its selection is already linked
    443                 IWorkbenchPage page = getSite().getPage();
    444                 try {
    445                     page.showView(IPageLayout.ID_PROP_SHEET, null, IWorkbenchPage.VIEW_ACTIVATE);
    446                 } catch (PartInitException e) {
    447                     AdtPlugin.log(e, "Could not activate property sheet");
    448                 }
    449                 */
    450 
    451                 TreeItem[] selection = getTreeViewer().getTree().getSelection();
    452                 if (selection.length > 0) {
    453                     CanvasViewInfo vi = getViewInfo(selection[0].getData());
    454                     if (vi != null) {
    455                         LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
    456                         canvas.show(vi);
    457                     }
    458                 }
    459             }
    460         });
    461 
    462         setupContextMenu();
    463 
    464         // Listen to selection changes from the layout editor
    465         getSite().getPage().addSelectionListener(this);
    466         getControl().addDisposeListener(new DisposeListener() {
    467 
    468             @Override
    469             public void widgetDisposed(DisposeEvent e) {
    470                 dispose();
    471             }
    472         });
    473 
    474         Tree tree = tv.getTree();
    475         tree.addKeyListener(new KeyListener() {
    476 
    477             @Override
    478             public void keyPressed(KeyEvent e) {
    479                 if (e.character == '-') {
    480                     if (mMoveUpAction.isEnabled()) {
    481                         mMoveUpAction.run();
    482                     }
    483                 } else if (e.character == '+') {
    484                     if (mMoveDownAction.isEnabled()) {
    485                         mMoveDownAction.run();
    486                     }
    487                 }
    488             }
    489 
    490             @Override
    491             public void keyReleased(KeyEvent e) {
    492             }
    493         });
    494 
    495         setupTooltip();
    496     }
    497 
    498     /**
    499      * This flag is true when the mouse button is being pressed somewhere inside
    500      * the property sheet
    501      */
    502     private boolean mPressInPropSheet;
    503 
    504     private void createPropertySheet() {
    505         mPropertySheetComposite = new PageSiteComposite(mControl, SWT.BORDER);
    506         mPropertySheetComposite.setTitleText("Properties");
    507         mPropertySheetComposite.setTitleImage(IconFactory.getInstance().getIcon("properties_view"));
    508         mPropertySheet = new PropertySheetPage(mGraphicalEditorPart);
    509         mPropertySheetComposite.setPage(mPropertySheet);
    510         if (AdtUtils.isEclipse4()) {
    511             mPropertySheet.getControl().addMouseListener(new MouseListener() {
    512                 @Override
    513                 public void mouseDown(MouseEvent e) {
    514                     mPressInPropSheet = true;
    515                 }
    516 
    517                 @Override
    518                 public void mouseUp(MouseEvent e) {
    519                     mPressInPropSheet = false;
    520                 }
    521 
    522                 @Override
    523                 public void mouseDoubleClick(MouseEvent e) {
    524                 }
    525             });
    526         }
    527     }
    528 
    529     @Override
    530     public void setFocus() {
    531         // Only call setFocus on the tree viewer if the mouse click isn't in the property
    532         // sheet area
    533         if (!mPressInPropSheet) {
    534             super.setFocus();
    535         }
    536     }
    537 
    538     @Override
    539     public void dispose() {
    540         mRootWrapper.setRoot(null);
    541 
    542         getSite().getPage().removeSelectionListener(this);
    543         super.dispose();
    544         if (mPropertySheet != null) {
    545             mPropertySheet.dispose();
    546             mPropertySheet = null;
    547         }
    548     }
    549 
    550     /**
    551      * Invoked by {@link LayoutCanvas} to set the model (a.k.a. the root view info).
    552      *
    553      * @param rootViewInfo The root of the view info hierarchy. Can be null.
    554      */
    555     public void setModel(CanvasViewInfo rootViewInfo) {
    556         if (!mActive) {
    557             return;
    558         }
    559 
    560         mRootWrapper.setRoot(rootViewInfo);
    561 
    562         TreeViewer tv = getTreeViewer();
    563         if (tv != null && !tv.getTree().isDisposed()) {
    564             Object[] expanded = tv.getExpandedElements();
    565             tv.refresh();
    566             tv.setExpandedElements(expanded);
    567             // Ensure that the root is expanded
    568             tv.expandToLevel(rootViewInfo, 2);
    569         }
    570     }
    571 
    572     /**
    573      * Returns the current tree viewer selection. Shouldn't be null,
    574      * although it can be {@link TreeSelection#EMPTY}.
    575      */
    576     @Override
    577     public ISelection getSelection() {
    578         return super.getSelection();
    579     }
    580 
    581     /**
    582      * Sets the outline selection.
    583      *
    584      * @param selection Only {@link ITreeSelection} will be used, otherwise the
    585      *   selection will be cleared (including a null selection).
    586      */
    587     @Override
    588     public void setSelection(ISelection selection) {
    589         // TreeViewer should be able to deal with a null selection, but let's make it safe
    590         if (selection == null) {
    591             selection = TreeSelection.EMPTY;
    592         }
    593         if (selection.equals(TreeSelection.EMPTY)) {
    594             return;
    595         }
    596 
    597         super.setSelection(selection);
    598 
    599         TreeViewer tv = getTreeViewer();
    600         if (tv == null || !(selection instanceof ITreeSelection) || selection.isEmpty()) {
    601             return;
    602         }
    603 
    604         // auto-reveal the selection
    605         ITreeSelection treeSel = (ITreeSelection) selection;
    606         for (TreePath p : treeSel.getPaths()) {
    607             tv.expandToLevel(p, 1);
    608         }
    609     }
    610 
    611     @Override
    612     protected void fireSelectionChanged(ISelection selection) {
    613         super.fireSelectionChanged(selection);
    614         if (mPropertySheet != null && !mIgnoreSelection) {
    615             mPropertySheet.selectionChanged(null, selection);
    616         }
    617     }
    618 
    619     /**
    620      * Listens to a workbench selection.
    621      * Only listen on selection coming from {@link LayoutEditorDelegate}, which avoid
    622      * picking up our own selections.
    623      */
    624     @Override
    625     public void selectionChanged(IWorkbenchPart part, ISelection selection) {
    626         if (mIgnoreSelection) {
    627             return;
    628         }
    629 
    630         if (part instanceof IEditorPart) {
    631             LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor((IEditorPart) part);
    632             if (delegate != null) {
    633                 try {
    634                     mIgnoreSelection = true;
    635                     setSelection(selection);
    636 
    637                     if (mPropertySheet != null) {
    638                         mPropertySheet.selectionChanged(part, selection);
    639                     }
    640                 } finally {
    641                     mIgnoreSelection = false;
    642                 }
    643             }
    644         }
    645     }
    646 
    647     @Override
    648     public void selectionChanged(SelectionChangedEvent event) {
    649         if (!mIgnoreSelection) {
    650             super.selectionChanged(event);
    651         }
    652     }
    653 
    654     // ----
    655 
    656     /**
    657      * In theory, the root of the model should be the input of the {@link TreeViewer},
    658      * which would be the root {@link CanvasViewInfo}.
    659      * That means in theory {@link ContentProvider#getElements(Object)} should return
    660      * its own input as the single root node.
    661      * <p/>
    662      * However as described in JFace Bug 9262, this case is not properly handled by
    663      * a {@link TreeViewer} and leads to an infinite recursion in the tree viewer.
    664      * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=9262
    665      * <p/>
    666      * The solution is to wrap the tree viewer input in a dummy root node that acts
    667      * as a parent. This class does just that.
    668      */
    669     private static class RootWrapper {
    670         private CanvasViewInfo mRoot;
    671 
    672         public void setRoot(CanvasViewInfo root) {
    673             mRoot = root;
    674         }
    675 
    676         public CanvasViewInfo getRoot() {
    677             return mRoot;
    678         }
    679     }
    680 
    681     /** Return the {@link CanvasViewInfo} associated with the given TreeItem's data field */
    682     /* package */ static CanvasViewInfo getViewInfo(Object viewData) {
    683         if (viewData instanceof RootWrapper) {
    684             return ((RootWrapper) viewData).getRoot();
    685         }
    686         if (viewData instanceof CanvasViewInfo) {
    687             return (CanvasViewInfo) viewData;
    688         }
    689         return null;
    690     }
    691 
    692     // --- Content and Label Providers ---
    693 
    694     /**
    695      * Content provider for the Outline model.
    696      * Objects are going to be {@link CanvasViewInfo}.
    697      */
    698     private static class ContentProvider implements ITreeContentProvider {
    699 
    700         @Override
    701         public Object[] getChildren(Object element) {
    702             if (element instanceof RootWrapper) {
    703                 CanvasViewInfo root = ((RootWrapper)element).getRoot();
    704                 if (root != null) {
    705                     return new Object[] { root };
    706                 }
    707             }
    708             if (element instanceof CanvasViewInfo) {
    709                 List<CanvasViewInfo> children = ((CanvasViewInfo) element).getUniqueChildren();
    710                 if (children != null) {
    711                     return children.toArray();
    712                 }
    713             }
    714             return new Object[0];
    715         }
    716 
    717         @Override
    718         public Object getParent(Object element) {
    719             if (element instanceof CanvasViewInfo) {
    720                 return ((CanvasViewInfo) element).getParent();
    721             }
    722             return null;
    723         }
    724 
    725         @Override
    726         public boolean hasChildren(Object element) {
    727             if (element instanceof CanvasViewInfo) {
    728                 List<CanvasViewInfo> children = ((CanvasViewInfo) element).getChildren();
    729                 if (children != null) {
    730                     return children.size() > 0;
    731                 }
    732             }
    733             return false;
    734         }
    735 
    736         /**
    737          * Returns the root element.
    738          * Semantically, the root element is the single top-level XML element of the XML layout.
    739          */
    740         @Override
    741         public Object[] getElements(Object inputElement) {
    742             return getChildren(inputElement);
    743         }
    744 
    745         @Override
    746         public void dispose() {
    747             // pass
    748         }
    749 
    750         @Override
    751         public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
    752             // pass
    753         }
    754     }
    755 
    756     /**
    757      * Label provider for the Outline model.
    758      * Objects are going to be {@link CanvasViewInfo}.
    759      */
    760     private class LabelProvider extends StyledCellLabelProvider {
    761         /**
    762          * Returns the element's logo with a fallback on the android logo.
    763          *
    764          * @param element the tree element
    765          * @return the image to be used as a logo
    766          */
    767         public Image getImage(Object element) {
    768             if (element instanceof CanvasViewInfo) {
    769                 element = ((CanvasViewInfo) element).getUiViewNode();
    770             }
    771 
    772             if (element instanceof UiViewElementNode) {
    773                 UiViewElementNode v = (UiViewElementNode) element;
    774                 return v.getIcon();
    775             }
    776 
    777             return AdtPlugin.getAndroidLogo();
    778         }
    779 
    780         /**
    781          * Uses {@link UiElementNode#getStyledDescription} for the label for this tree item.
    782          */
    783         @Override
    784         public void update(ViewerCell cell) {
    785             Object element = cell.getElement();
    786             StyledString styledString = null;
    787 
    788             CanvasViewInfo vi = null;
    789             if (element instanceof CanvasViewInfo) {
    790                 vi = (CanvasViewInfo) element;
    791                 element = vi.getUiViewNode();
    792             }
    793 
    794             Image image = getImage(element);
    795 
    796             if (element instanceof UiElementNode) {
    797                 UiElementNode node = (UiElementNode) element;
    798                 styledString = node.getStyledDescription();
    799                 Node xmlNode = node.getXmlNode();
    800                 if (xmlNode instanceof Element) {
    801                     Element e = (Element) xmlNode;
    802 
    803                     // Temporary diagnostics code when developing GridLayout
    804                     if (GridLayoutRule.sDebugGridLayout) {
    805 
    806                         String namespace;
    807                         if (e.getNodeName().equals(GRID_LAYOUT) ||
    808                                 e.getParentNode() != null
    809                                 && e.getParentNode().getNodeName().equals(GRID_LAYOUT)) {
    810                             namespace = ANDROID_URI;
    811                         } else {
    812                             // Else: probably a v7 gridlayout
    813                             IProject project = mGraphicalEditorPart.getProject();
    814                             ProjectState projectState = Sdk.getProjectState(project);
    815                             if (projectState != null && projectState.isLibrary()) {
    816                                 namespace = AUTO_URI;
    817                             } else {
    818                                 ManifestInfo info = ManifestInfo.get(project);
    819                                 namespace = URI_PREFIX + info.getPackage();
    820                             }
    821                         }
    822 
    823                         if (e.getNodeName() != null && e.getNodeName().endsWith(GRID_LAYOUT)) {
    824                             // Attach rowCount/columnCount info
    825                             String rowCount = e.getAttributeNS(namespace, ATTR_ROW_COUNT);
    826                             if (rowCount.length() == 0) {
    827                                 rowCount = "?";
    828                             }
    829                             String columnCount = e.getAttributeNS(namespace, ATTR_COLUMN_COUNT);
    830                             if (columnCount.length() == 0) {
    831                                 columnCount = "?";
    832                             }
    833 
    834                             styledString.append(" - columnCount=", QUALIFIER_STYLER);
    835                             styledString.append(columnCount, QUALIFIER_STYLER);
    836                             styledString.append(", rowCount=", QUALIFIER_STYLER);
    837                             styledString.append(rowCount, QUALIFIER_STYLER);
    838                         } else if (e.getParentNode() != null
    839                             && e.getParentNode().getNodeName() != null
    840                             && e.getParentNode().getNodeName().endsWith(GRID_LAYOUT)) {
    841                             // Attach row/column info
    842                             String row = e.getAttributeNS(namespace, ATTR_LAYOUT_ROW);
    843                             if (row.length() == 0) {
    844                                 row = "?";
    845                             }
    846                             Styler colStyle = QUALIFIER_STYLER;
    847                             String column = e.getAttributeNS(namespace, ATTR_LAYOUT_COLUMN);
    848                             if (column.length() == 0) {
    849                                 column = "?";
    850                             } else {
    851                                 String colCount = ((Element) e.getParentNode()).getAttributeNS(
    852                                         namespace, ATTR_COLUMN_COUNT);
    853                                 if (colCount.length() > 0 && Integer.parseInt(colCount) <=
    854                                         Integer.parseInt(column)) {
    855                                     colStyle = StyledString.createColorRegistryStyler(
    856                                         JFacePreferences.ERROR_COLOR, null);
    857                                 }
    858                             }
    859                             String rowSpan = e.getAttributeNS(namespace, ATTR_LAYOUT_ROW_SPAN);
    860                             String columnSpan = e.getAttributeNS(namespace,
    861                                     ATTR_LAYOUT_COLUMN_SPAN);
    862                             if (rowSpan.length() == 0) {
    863                                 rowSpan = "1";
    864                             }
    865                             if (columnSpan.length() == 0) {
    866                                 columnSpan = "1";
    867                             }
    868 
    869                             styledString.append(" - cell (row=", QUALIFIER_STYLER);
    870                             styledString.append(row, QUALIFIER_STYLER);
    871                             styledString.append(',', QUALIFIER_STYLER);
    872                             styledString.append("col=", colStyle);
    873                             styledString.append(column, colStyle);
    874                             styledString.append(')', colStyle);
    875                             styledString.append(", span=(", QUALIFIER_STYLER);
    876                             styledString.append(columnSpan, QUALIFIER_STYLER);
    877                             styledString.append(',', QUALIFIER_STYLER);
    878                             styledString.append(rowSpan, QUALIFIER_STYLER);
    879                             styledString.append(')', QUALIFIER_STYLER);
    880 
    881                             String gravity = e.getAttributeNS(namespace, ATTR_LAYOUT_GRAVITY);
    882                             if (gravity != null && gravity.length() > 0) {
    883                                 styledString.append(" : ", COUNTER_STYLER);
    884                                 styledString.append(gravity, COUNTER_STYLER);
    885                             }
    886 
    887                         }
    888                     }
    889 
    890                     if (e.hasAttributeNS(ANDROID_URI, ATTR_TEXT)) {
    891                         // Show the text attribute
    892                         String text = e.getAttributeNS(ANDROID_URI, ATTR_TEXT);
    893                         if (text != null && text.length() > 0
    894                                 && !text.contains(node.getDescriptor().getUiName())) {
    895                             if (text.charAt(0) == '@') {
    896                                 String resolved = mGraphicalEditorPart.findString(text);
    897                                 if (resolved != null) {
    898                                     text = resolved;
    899                                 }
    900                             }
    901                             if (styledString.length() < LABEL_MAX_WIDTH - LABEL_SEPARATOR.length()
    902                                     - 2) {
    903                                 styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
    904 
    905                                 styledString.append('"', QUALIFIER_STYLER);
    906                                 styledString.append(truncate(text, styledString), QUALIFIER_STYLER);
    907                                 styledString.append('"', QUALIFIER_STYLER);
    908                             }
    909                         }
    910                     } else if (e.hasAttributeNS(ANDROID_URI, ATTR_SRC)) {
    911                         // Show ImageView source attributes etc
    912                         String src = e.getAttributeNS(ANDROID_URI, ATTR_SRC);
    913                         if (src != null && src.length() > 0) {
    914                             if (src.startsWith(DRAWABLE_PREFIX)) {
    915                                 src = src.substring(DRAWABLE_PREFIX.length());
    916                             }
    917                             styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
    918                             styledString.append(truncate(src, styledString), QUALIFIER_STYLER);
    919                         }
    920                     } else if (e.getTagName().equals(SdkConstants.VIEW_INCLUDE)) {
    921                         // Show the include reference.
    922 
    923                         // Note: the layout attribute is NOT in the Android namespace
    924                         String src = e.getAttribute(SdkConstants.ATTR_LAYOUT);
    925                         if (src != null && src.length() > 0) {
    926                             if (src.startsWith(LAYOUT_RESOURCE_PREFIX)) {
    927                                 src = src.substring(LAYOUT_RESOURCE_PREFIX.length());
    928                             }
    929                             styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
    930                             styledString.append(truncate(src, styledString), QUALIFIER_STYLER);
    931                         }
    932                     }
    933                 }
    934             } else if (element == null && vi != null) {
    935                 // It's an inclusion-context: display it
    936                 Reference includedWithin = mGraphicalEditorPart.getIncludedWithin();
    937                 if (includedWithin != null) {
    938                     styledString = new StyledString();
    939                     styledString.append(includedWithin.getDisplayName(), QUALIFIER_STYLER);
    940                     image = IconFactory.getInstance().getIcon(SdkConstants.VIEW_INCLUDE);
    941                 }
    942             }
    943 
    944             if (styledString == null) {
    945                 styledString = new StyledString();
    946                 styledString.append(element == null ? "(null)" : element.toString());
    947             }
    948 
    949            cell.setText(styledString.toString());
    950            cell.setStyleRanges(styledString.getStyleRanges());
    951            cell.setImage(image);
    952            super.update(cell);
    953        }
    954 
    955         @Override
    956         public boolean isLabelProperty(Object element, String property) {
    957             return super.isLabelProperty(element, property);
    958         }
    959     }
    960 
    961     // --- Context Menu ---
    962 
    963     /**
    964      * This viewer uses its own actions that delegate to the ones given
    965      * by the {@link LayoutCanvas}. All the processing is actually handled
    966      * directly by the canvas and this viewer only gets refreshed as a
    967      * consequence of the canvas changing the XML model.
    968      */
    969     private void setupContextMenu() {
    970 
    971         mMenuManager = new MenuManager();
    972         mMenuManager.removeAll();
    973 
    974         mMenuManager.add(mMoveUpAction);
    975         mMenuManager.add(mMoveDownAction);
    976         mMenuManager.add(new Separator());
    977 
    978         mMenuManager.add(new SelectionManager.SelectionMenu(mGraphicalEditorPart));
    979         mMenuManager.add(new Separator());
    980         final String prefix = LayoutCanvas.PREFIX_CANVAS_ACTION;
    981         mMenuManager.add(new DelegateAction(prefix + ActionFactory.CUT.getId()));
    982         mMenuManager.add(new DelegateAction(prefix + ActionFactory.COPY.getId()));
    983         mMenuManager.add(new DelegateAction(prefix + ActionFactory.PASTE.getId()));
    984 
    985         mMenuManager.add(new Separator());
    986 
    987         mMenuManager.add(new DelegateAction(prefix + ActionFactory.DELETE.getId()));
    988 
    989         mMenuManager.addMenuListener(new IMenuListener() {
    990             @Override
    991             public void menuAboutToShow(IMenuManager manager) {
    992                 // Update all actions to match their LayoutCanvas counterparts
    993                 for (IContributionItem contrib : manager.getItems()) {
    994                     if (contrib instanceof ActionContributionItem) {
    995                         IAction action = ((ActionContributionItem) contrib).getAction();
    996                         if (action instanceof DelegateAction) {
    997                             ((DelegateAction) action).updateFromEditorPart(mGraphicalEditorPart);
    998                         }
    999                     }
   1000                 }
   1001             }
   1002         });
   1003 
   1004         new DynamicContextMenu(
   1005                 mGraphicalEditorPart.getEditorDelegate(),
   1006                 mGraphicalEditorPart.getCanvasControl(),
   1007                 mMenuManager);
   1008 
   1009         getTreeViewer().getTree().setMenu(mMenuManager.createContextMenu(getControl()));
   1010 
   1011         // Update Move Up/Move Down state only when the menu is opened
   1012         getTreeViewer().getTree().addMenuDetectListener(new MenuDetectListener() {
   1013             @Override
   1014             public void menuDetected(MenuDetectEvent e) {
   1015                 mMenuManager.update(IAction.ENABLED);
   1016             }
   1017         });
   1018     }
   1019 
   1020     /**
   1021      * An action that delegates its properties and behavior to a target action.
   1022      * The target action can be null or it can change overtime, typically as the
   1023      * layout canvas' editor part is activated or closed.
   1024      */
   1025     private static class DelegateAction extends Action {
   1026         private IAction mTargetAction;
   1027         private final String mCanvasActionId;
   1028 
   1029         public DelegateAction(String canvasActionId) {
   1030             super(canvasActionId);
   1031             setId(canvasActionId);
   1032             mCanvasActionId = canvasActionId;
   1033         }
   1034 
   1035         // --- Methods form IAction ---
   1036 
   1037         /** Returns the target action's {@link #isEnabled()} if defined, or false. */
   1038         @Override
   1039         public boolean isEnabled() {
   1040             return mTargetAction == null ? false : mTargetAction.isEnabled();
   1041         }
   1042 
   1043         /** Returns the target action's {@link #isChecked()} if defined, or false. */
   1044         @Override
   1045         public boolean isChecked() {
   1046             return mTargetAction == null ? false : mTargetAction.isChecked();
   1047         }
   1048 
   1049         /** Returns the target action's {@link #isHandled()} if defined, or false. */
   1050         @Override
   1051         public boolean isHandled() {
   1052             return mTargetAction == null ? false : mTargetAction.isHandled();
   1053         }
   1054 
   1055         /** Runs the target action if defined. */
   1056         @Override
   1057         public void run() {
   1058             if (mTargetAction != null) {
   1059                 mTargetAction.run();
   1060             }
   1061             super.run();
   1062         }
   1063 
   1064         /**
   1065          * Updates this action to delegate to its counterpart in the given editor part
   1066          *
   1067          * @param editorPart The editor being updated
   1068          */
   1069         public void updateFromEditorPart(GraphicalEditorPart editorPart) {
   1070             LayoutCanvas canvas = editorPart == null ? null : editorPart.getCanvasControl();
   1071             if (canvas == null) {
   1072                 mTargetAction = null;
   1073             } else {
   1074                 mTargetAction = canvas.getAction(mCanvasActionId);
   1075             }
   1076 
   1077             if (mTargetAction != null) {
   1078                 setText(mTargetAction.getText());
   1079                 setId(mTargetAction.getId());
   1080                 setDescription(mTargetAction.getDescription());
   1081                 setImageDescriptor(mTargetAction.getImageDescriptor());
   1082                 setHoverImageDescriptor(mTargetAction.getHoverImageDescriptor());
   1083                 setDisabledImageDescriptor(mTargetAction.getDisabledImageDescriptor());
   1084                 setToolTipText(mTargetAction.getToolTipText());
   1085                 setActionDefinitionId(mTargetAction.getActionDefinitionId());
   1086                 setHelpListener(mTargetAction.getHelpListener());
   1087                 setAccelerator(mTargetAction.getAccelerator());
   1088                 setChecked(mTargetAction.isChecked());
   1089                 setEnabled(mTargetAction.isEnabled());
   1090             } else {
   1091                 setEnabled(false);
   1092             }
   1093         }
   1094     }
   1095 
   1096     /** Returns the associated editor with this outline */
   1097     /* package */GraphicalEditorPart getEditor() {
   1098         return mGraphicalEditorPart;
   1099     }
   1100 
   1101     @Override
   1102     public void setActionBars(IActionBars actionBars) {
   1103         super.setActionBars(actionBars);
   1104 
   1105         // Map Outline actions to canvas actions such that they share Undo context etc
   1106         LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
   1107         canvas.updateGlobalActions(actionBars);
   1108 
   1109         // Special handling for Select All since it's different than the canvas (will
   1110         // include selecting the root etc)
   1111         actionBars.setGlobalActionHandler(mTreeSelectAllAction.getId(), mTreeSelectAllAction);
   1112         actionBars.updateActionBars();
   1113     }
   1114 
   1115     // ---- Move Up/Down Support ----
   1116 
   1117     /** Returns true if the current selected item can be moved */
   1118     private boolean canMove(boolean forward) {
   1119         CanvasViewInfo viewInfo = getSingleSelectedItem();
   1120         if (viewInfo != null) {
   1121             UiViewElementNode node = viewInfo.getUiViewNode();
   1122             if (forward) {
   1123                 return findNext(node) != null;
   1124             } else {
   1125                 return findPrevious(node) != null;
   1126             }
   1127         }
   1128 
   1129         return false;
   1130     }
   1131 
   1132     /** Moves the current selected item down (forward) or up (not forward) */
   1133     private void move(boolean forward) {
   1134         CanvasViewInfo viewInfo = getSingleSelectedItem();
   1135         if (viewInfo != null) {
   1136             final Pair<UiViewElementNode, Integer> target;
   1137             UiViewElementNode selected = viewInfo.getUiViewNode();
   1138             if (forward) {
   1139                 target = findNext(selected);
   1140             } else {
   1141                 target = findPrevious(selected);
   1142             }
   1143             if (target != null) {
   1144                 final LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
   1145                 final SelectionManager selectionManager = canvas.getSelectionManager();
   1146                 final ArrayList<SelectionItem> dragSelection = new ArrayList<SelectionItem>();
   1147                 dragSelection.add(selectionManager.createSelection(viewInfo));
   1148                 SelectionManager.sanitize(dragSelection);
   1149 
   1150                 if (!dragSelection.isEmpty()) {
   1151                     final SimpleElement[] elements = SelectionItem.getAsElements(dragSelection);
   1152                     UiViewElementNode parentNode = target.getFirst();
   1153                     final NodeProxy targetNode = canvas.getNodeFactory().create(parentNode);
   1154 
   1155                     // Record children of the target right before the drop (such that we
   1156                     // can find out after the drop which exact children were inserted)
   1157                     Set<INode> children = new HashSet<INode>();
   1158                     for (INode node : targetNode.getChildren()) {
   1159                         children.add(node);
   1160                     }
   1161 
   1162                     String label = MoveGesture.computeUndoLabel(targetNode,
   1163                             elements, DND.DROP_MOVE);
   1164                     canvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(label, new Runnable() {
   1165                         @Override
   1166                         public void run() {
   1167                             InsertType insertType = InsertType.MOVE_INTO;
   1168                             if (dragSelection.get(0).getNode().getParent() == targetNode) {
   1169                                 insertType = InsertType.MOVE_WITHIN;
   1170                             }
   1171                             canvas.getRulesEngine().setInsertType(insertType);
   1172                             int index = target.getSecond();
   1173                             BaseLayoutRule.insertAt(targetNode, elements, false, index);
   1174                             targetNode.applyPendingChanges();
   1175                             canvas.getClipboardSupport().deleteSelection("Remove", dragSelection);
   1176                         }
   1177                     });
   1178 
   1179                     // Now find out which nodes were added, and look up their
   1180                     // corresponding CanvasViewInfos
   1181                     final List<INode> added = new ArrayList<INode>();
   1182                     for (INode node : targetNode.getChildren()) {
   1183                         if (!children.contains(node)) {
   1184                             added.add(node);
   1185                         }
   1186                     }
   1187 
   1188                     selectionManager.setOutlineSelection(added);
   1189                 }
   1190             }
   1191         }
   1192     }
   1193 
   1194     /**
   1195      * Returns the {@link CanvasViewInfo} for the currently selected item, or null if
   1196      * there are no or multiple selected items
   1197      *
   1198      * @return the current selected item if there is exactly one item selected
   1199      */
   1200     private CanvasViewInfo getSingleSelectedItem() {
   1201         TreeItem[] selection = getTreeViewer().getTree().getSelection();
   1202         if (selection.length == 1) {
   1203             return getViewInfo(selection[0].getData());
   1204         }
   1205 
   1206         return null;
   1207     }
   1208 
   1209 
   1210     /** Returns the pair [parent,index] of the next node (when iterating forward) */
   1211     @VisibleForTesting
   1212     /* package */ static Pair<UiViewElementNode, Integer> findNext(UiViewElementNode node) {
   1213         UiElementNode parent = node.getUiParent();
   1214         if (parent == null) {
   1215             return null;
   1216         }
   1217 
   1218         UiElementNode next = node.getUiNextSibling();
   1219         if (next != null) {
   1220             if (DescriptorsUtils.canInsertChildren(next.getDescriptor(), null)) {
   1221                 return getFirstPosition(next);
   1222             } else {
   1223                 return getPositionAfter(next);
   1224             }
   1225         }
   1226 
   1227         next = parent.getUiNextSibling();
   1228         if (next != null) {
   1229             return getPositionBefore(next);
   1230         } else {
   1231             UiElementNode grandParent = parent.getUiParent();
   1232             if (grandParent != null) {
   1233                 return getLastPosition(grandParent);
   1234             }
   1235         }
   1236 
   1237         return null;
   1238     }
   1239 
   1240     /** Returns the pair [parent,index] of the previous node (when iterating backward) */
   1241     @VisibleForTesting
   1242     /* package */ static Pair<UiViewElementNode, Integer> findPrevious(UiViewElementNode node) {
   1243         UiElementNode prev = node.getUiPreviousSibling();
   1244         if (prev != null) {
   1245             UiElementNode curr = prev;
   1246             while (true) {
   1247                 List<UiElementNode> children = curr.getUiChildren();
   1248                 if (children.size() > 0) {
   1249                     curr = children.get(children.size() - 1);
   1250                     continue;
   1251                 }
   1252                 if (DescriptorsUtils.canInsertChildren(curr.getDescriptor(), null)) {
   1253                     return getFirstPosition(curr);
   1254                 } else {
   1255                     if (curr == prev) {
   1256                         return getPositionBefore(curr);
   1257                     } else {
   1258                         return getPositionAfter(curr);
   1259                     }
   1260                 }
   1261             }
   1262         }
   1263 
   1264         return getPositionBefore(node.getUiParent());
   1265     }
   1266 
   1267     /** Returns the pair [parent,index] of the position immediately before the given node  */
   1268     private static Pair<UiViewElementNode, Integer> getPositionBefore(UiElementNode node) {
   1269         if (node != null) {
   1270             UiElementNode parent = node.getUiParent();
   1271             if (parent != null && parent instanceof UiViewElementNode) {
   1272                 return Pair.of((UiViewElementNode) parent, node.getUiSiblingIndex());
   1273             }
   1274         }
   1275 
   1276         return null;
   1277     }
   1278 
   1279     /** Returns the pair [parent,index] of the position immediately following the given node  */
   1280     private static Pair<UiViewElementNode, Integer> getPositionAfter(UiElementNode node) {
   1281         if (node != null) {
   1282             UiElementNode parent = node.getUiParent();
   1283             if (parent != null && parent instanceof UiViewElementNode) {
   1284                 return Pair.of((UiViewElementNode) parent, node.getUiSiblingIndex() + 1);
   1285             }
   1286         }
   1287 
   1288         return null;
   1289     }
   1290 
   1291     /** Returns the pair [parent,index] of the first position inside the given parent */
   1292     private static Pair<UiViewElementNode, Integer> getFirstPosition(UiElementNode parent) {
   1293         if (parent != null && parent instanceof UiViewElementNode) {
   1294             return Pair.of((UiViewElementNode) parent, 0);
   1295         }
   1296 
   1297         return null;
   1298     }
   1299 
   1300     /**
   1301      * Returns the pair [parent,index] of the last position after the given node's
   1302      * children
   1303      */
   1304     private static Pair<UiViewElementNode, Integer> getLastPosition(UiElementNode parent) {
   1305         if (parent != null && parent instanceof UiViewElementNode) {
   1306             return Pair.of((UiViewElementNode) parent, parent.getUiChildren().size());
   1307         }
   1308 
   1309         return null;
   1310     }
   1311 
   1312     /**
   1313      * Truncates the given text such that it will fit into the given {@link StyledString}
   1314      * up to a maximum length of {@link #LABEL_MAX_WIDTH}.
   1315      *
   1316      * @param text the text to truncate
   1317      * @param string the existing string to be appended to
   1318      * @return the truncated string
   1319      */
   1320     private static String truncate(String text, StyledString string) {
   1321         int existingLength = string.length();
   1322 
   1323         if (text.length() + existingLength > LABEL_MAX_WIDTH) {
   1324             int truncatedLength = LABEL_MAX_WIDTH - existingLength - 3;
   1325             if (truncatedLength > 0) {
   1326                 return String.format("%1$s...", text.substring(0, truncatedLength));
   1327             } else {
   1328                 return ""; //$NON-NLS-1$
   1329             }
   1330         }
   1331 
   1332         return text;
   1333     }
   1334 
   1335     @Override
   1336     public void setToolBar(IToolBarManager toolBarManager) {
   1337         makeContributions(null, toolBarManager, null);
   1338         toolBarManager.update(false);
   1339     }
   1340 
   1341     /**
   1342      * Sets up a custom tooltip when hovering over tree items. It currently displays the error
   1343      * message for the lint warning associated with each node, if any (and only if the hover
   1344      * is over the icon portion).
   1345      */
   1346     private void setupTooltip() {
   1347         final Tree tree = getTreeViewer().getTree();
   1348 
   1349         // This is based on SWT Snippet 125
   1350         final Listener listener = new Listener() {
   1351             Shell mTip = null;
   1352             Label mLabel  = null;
   1353 
   1354             @Override
   1355             public void handleEvent(Event event) {
   1356                 switch(event.type) {
   1357                 case SWT.Dispose:
   1358                 case SWT.KeyDown:
   1359                 case SWT.MouseExit:
   1360                 case SWT.MouseDown:
   1361                 case SWT.MouseMove:
   1362                     if (mTip != null) {
   1363                         mTip.dispose();
   1364                         mTip = null;
   1365                         mLabel = null;
   1366                     }
   1367                     break;
   1368                 case SWT.MouseHover:
   1369                     if (mTip != null) {
   1370                         mTip.dispose();
   1371                         mTip = null;
   1372                         mLabel = null;
   1373                     }
   1374 
   1375                     String tooltip = null;
   1376 
   1377                     TreeItem item = tree.getItem(new Point(event.x, event.y));
   1378                     if (item != null) {
   1379                         Rectangle rect = item.getBounds(0);
   1380                         if (event.x - rect.x > 16) { // 16: Standard width of our outline icons
   1381                             return;
   1382                         }
   1383 
   1384                         Object data = item.getData();
   1385                         if (data != null && data instanceof CanvasViewInfo) {
   1386                             LayoutEditorDelegate editor = mGraphicalEditorPart.getEditorDelegate();
   1387                             CanvasViewInfo vi = (CanvasViewInfo) data;
   1388                             IMarker marker = editor.getIssueForNode(vi.getUiViewNode());
   1389                             if (marker != null) {
   1390                                 tooltip = marker.getAttribute(IMarker.MESSAGE, null);
   1391                             }
   1392                         }
   1393 
   1394                         if (tooltip != null) {
   1395                             Shell shell = tree.getShell();
   1396                             Display display = tree.getDisplay();
   1397 
   1398                             Color fg = display.getSystemColor(SWT.COLOR_INFO_FOREGROUND);
   1399                             Color bg = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND);
   1400                             mTip = new Shell(shell, SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL);
   1401                             mTip.setBackground(bg);
   1402                             FillLayout layout = new FillLayout();
   1403                             layout.marginWidth = 1;
   1404                             layout.marginHeight = 1;
   1405                             mTip.setLayout(layout);
   1406                             mLabel = new Label(mTip, SWT.WRAP);
   1407                             mLabel.setForeground(fg);
   1408                             mLabel.setBackground(bg);
   1409                             mLabel.setText(tooltip);
   1410                             mLabel.addListener(SWT.MouseExit, this);
   1411                             mLabel.addListener(SWT.MouseDown, this);
   1412 
   1413                             Point pt = tree.toDisplay(rect.x, rect.y + rect.height);
   1414                             Rectangle displayBounds = display.getBounds();
   1415                             // -10: Don't extend -all- the way to the edge of the screen
   1416                             // which would make it look like it has been cropped
   1417                             int availableWidth = displayBounds.x + displayBounds.width - pt.x - 10;
   1418                             if (availableWidth < 80) {
   1419                                 availableWidth = 80;
   1420                             }
   1421                             Point size = mTip.computeSize(SWT.DEFAULT, SWT.DEFAULT);
   1422                             if (size.x > availableWidth) {
   1423                                 size = mTip.computeSize(availableWidth, SWT.DEFAULT);
   1424                             }
   1425                             mTip.setBounds(pt.x, pt.y, size.x, size.y);
   1426 
   1427                             mTip.setVisible(true);
   1428                         }
   1429                     }
   1430                 }
   1431             }
   1432         };
   1433 
   1434         tree.addListener(SWT.Dispose, listener);
   1435         tree.addListener(SWT.KeyDown, listener);
   1436         tree.addListener(SWT.MouseMove, listener);
   1437         tree.addListener(SWT.MouseHover, listener);
   1438     }
   1439 }
   1440