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      */
OutlinePage(GraphicalEditorPart graphicalEditorPart)233     public OutlinePage(GraphicalEditorPart graphicalEditorPart) {
234         super();
235         mGraphicalEditorPart = graphicalEditorPart;
236     }
237 
238     @Override
getControl()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 
setActive(boolean active)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 */
refreshIcons()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      */
setShowHeader(boolean show)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      */
setShowPropertySheet(boolean show)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
createControl(Composite parent)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 
createOutline(Composite parent)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 
createPropertySheet()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
setFocus()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
dispose()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      */
setModel(CanvasViewInfo rootViewInfo)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
getSelection()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
setSelection(ISelection selection)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
fireSelectionChanged(ISelection selection)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
selectionChanged(IWorkbenchPart part, ISelection selection)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
selectionChanged(SelectionChangedEvent event)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 
setRoot(CanvasViewInfo root)672         public void setRoot(CanvasViewInfo root) {
673             mRoot = root;
674         }
675 
getRoot()676         public CanvasViewInfo getRoot() {
677             return mRoot;
678         }
679     }
680 
681     /** Return the {@link CanvasViewInfo} associated with the given TreeItem's data field */
getViewInfo(Object viewData)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
getChildren(Object element)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
getParent(Object element)718         public Object getParent(Object element) {
719             if (element instanceof CanvasViewInfo) {
720                 return ((CanvasViewInfo) element).getParent();
721             }
722             return null;
723         }
724 
725         @Override
hasChildren(Object element)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
getElements(Object inputElement)741         public Object[] getElements(Object inputElement) {
742             return getChildren(inputElement);
743         }
744 
745         @Override
dispose()746         public void dispose() {
747             // pass
748         }
749 
750         @Override
inputChanged(Viewer viewer, Object oldInput, Object newInput)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          */
getImage(Object element)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
update(ViewerCell cell)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
isLabelProperty(Object element, String property)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      */
setupContextMenu()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 
DelegateAction(String canvasActionId)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
isEnabled()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
isChecked()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
isHandled()1051         public boolean isHandled() {
1052             return mTargetAction == null ? false : mTargetAction.isHandled();
1053         }
1054 
1055         /** Runs the target action if defined. */
1056         @Override
run()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          */
updateFromEditorPart(GraphicalEditorPart editorPart)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 */
getEditor()1097     /* package */GraphicalEditorPart getEditor() {
1098         return mGraphicalEditorPart;
1099     }
1100 
1101     @Override
setActionBars(IActionBars actionBars)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 */
canMove(boolean forward)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) */
move(boolean 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      */
getSingleSelectedItem()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
findNext(UiViewElementNode node)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
findPrevious(UiViewElementNode node)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  */
getPositionBefore(UiElementNode 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  */
getPositionAfter(UiElementNode 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 */
getFirstPosition(UiElementNode 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      */
getLastPosition(UiElementNode parent)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      */
truncate(String text, StyledString string)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
setToolBar(IToolBarManager toolBarManager)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      */
setupTooltip()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