• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 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 com.android.ide.common.api.INode;
20 import com.android.ide.common.api.Margins;
21 import com.android.ide.common.api.Point;
22 import com.android.ide.common.layout.LayoutConstants;
23 import com.android.ide.common.rendering.api.Capability;
24 import com.android.ide.common.rendering.api.RenderSession;
25 import com.android.ide.eclipse.adt.AdtPlugin;
26 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
27 import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
28 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
29 import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;
30 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
31 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
32 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
33 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
34 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
35 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
36 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
37 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
38 import com.android.resources.Density;
39 import com.android.sdklib.SdkConstants;
40 
41 import org.eclipse.core.filesystem.EFS;
42 import org.eclipse.core.filesystem.IFileStore;
43 import org.eclipse.core.resources.IFile;
44 import org.eclipse.core.resources.IWorkspaceRoot;
45 import org.eclipse.core.resources.ResourcesPlugin;
46 import org.eclipse.core.runtime.CoreException;
47 import org.eclipse.core.runtime.IPath;
48 import org.eclipse.core.runtime.QualifiedName;
49 import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
50 import org.eclipse.jface.action.Action;
51 import org.eclipse.jface.action.ActionContributionItem;
52 import org.eclipse.jface.action.IAction;
53 import org.eclipse.jface.action.IContributionItem;
54 import org.eclipse.jface.action.IMenuManager;
55 import org.eclipse.jface.action.IStatusLineManager;
56 import org.eclipse.jface.action.MenuManager;
57 import org.eclipse.jface.action.Separator;
58 import org.eclipse.swt.SWT;
59 import org.eclipse.swt.custom.StyledText;
60 import org.eclipse.swt.dnd.DND;
61 import org.eclipse.swt.dnd.DragSource;
62 import org.eclipse.swt.dnd.DropTarget;
63 import org.eclipse.swt.dnd.TextTransfer;
64 import org.eclipse.swt.dnd.Transfer;
65 import org.eclipse.swt.events.ControlAdapter;
66 import org.eclipse.swt.events.ControlEvent;
67 import org.eclipse.swt.events.KeyEvent;
68 import org.eclipse.swt.events.MenuDetectEvent;
69 import org.eclipse.swt.events.MenuDetectListener;
70 import org.eclipse.swt.events.MouseEvent;
71 import org.eclipse.swt.events.PaintEvent;
72 import org.eclipse.swt.events.PaintListener;
73 import org.eclipse.swt.graphics.Font;
74 import org.eclipse.swt.graphics.GC;
75 import org.eclipse.swt.graphics.Image;
76 import org.eclipse.swt.graphics.ImageData;
77 import org.eclipse.swt.graphics.Rectangle;
78 import org.eclipse.swt.widgets.Canvas;
79 import org.eclipse.swt.widgets.Composite;
80 import org.eclipse.swt.widgets.Control;
81 import org.eclipse.swt.widgets.Display;
82 import org.eclipse.swt.widgets.Menu;
83 import org.eclipse.ui.IActionBars;
84 import org.eclipse.ui.IEditorPart;
85 import org.eclipse.ui.IEditorSite;
86 import org.eclipse.ui.IWorkbenchPage;
87 import org.eclipse.ui.PartInitException;
88 import org.eclipse.ui.actions.ActionFactory;
89 import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction;
90 import org.eclipse.ui.actions.ContributionItemFactory;
91 import org.eclipse.ui.ide.IDE;
92 import org.eclipse.ui.internal.ide.IDEWorkbenchMessages;
93 import org.eclipse.ui.texteditor.ITextEditor;
94 import org.w3c.dom.Node;
95 
96 import java.util.HashSet;
97 import java.util.List;
98 import java.util.Set;
99 
100 /**
101  * Displays the image rendered by the {@link GraphicalEditorPart} and handles
102  * the interaction with the widgets.
103  * <p/>
104  * {@link LayoutCanvas} implements the "Canvas" control. The editor part
105  * actually uses the {@link LayoutCanvasViewer}, which is a JFace viewer wrapper
106  * around this control.
107  * <p/>
108  * The LayoutCanvas contains the painting logic for the canvas. Selection,
109  * clipboard, view management etc. is handled in separate helper classes.
110  *
111  * @since GLE2
112  */
113 @SuppressWarnings("restriction") // For WorkBench "Show In" support
114 public class LayoutCanvas extends Canvas {
115     private final static QualifiedName NAME_ZOOM =
116         new QualifiedName(AdtPlugin.PLUGIN_ID, "zoom");//$NON-NLS-1$
117 
118     private static final boolean DEBUG = false;
119 
120     /* package */ static final String PREFIX_CANVAS_ACTION = "canvas_action_";
121 
122     /** The layout editor that uses this layout canvas. */
123     private final LayoutEditorDelegate mEditorDelegate;
124 
125     /** The Rules Engine, associated with the current project. */
126     private RulesEngine mRulesEngine;
127 
128     /** GC wrapper given to the IViewRule methods. The GC itself is only defined in the
129      *  context of {@link #onPaint(PaintEvent)}; otherwise it is null. */
130     private GCWrapper mGCWrapper;
131 
132     /** Default font used on the canvas. Do not dispose, it's a system font. */
133     private Font mFont;
134 
135     /** Current hover view info. Null when no mouse hover. */
136     private CanvasViewInfo mHoverViewInfo;
137 
138     /** When true, always display the outline of all views. */
139     private boolean mShowOutline;
140 
141     /** When true, display the outline of all empty parent views. */
142     private boolean mShowInvisible;
143 
144     /** Drop target associated with this composite. */
145     private DropTarget mDropTarget;
146 
147     /** Factory that can create {@link INode} proxies. */
148     private final NodeFactory mNodeFactory = new NodeFactory(this);
149 
150     /** Vertical scaling & scrollbar information. */
151     private CanvasTransform mVScale;
152 
153     /** Horizontal scaling & scrollbar information. */
154     private CanvasTransform mHScale;
155 
156     /** Drag source associated with this canvas. */
157     private DragSource mDragSource;
158 
159     /**
160      * The current Outline Page, to set its model.
161      * It isn't possible to call OutlinePage2.dispose() in this.dispose().
162      * this.dispose() is called from GraphicalEditorPart.dispose(),
163      * when page's widget is already disposed.
164      * Added the DisposeListener to OutlinePage2 in order to correctly dispose this page.
165      **/
166     private OutlinePage mOutlinePage;
167 
168     /** Delete action for the Edit or context menu. */
169     private Action mDeleteAction;
170 
171     /** Select-All action for the Edit or context menu. */
172     private Action mSelectAllAction;
173 
174     /** Paste action for the Edit or context menu. */
175     private Action mPasteAction;
176 
177     /** Cut action for the Edit or context menu. */
178     private Action mCutAction;
179 
180     /** Copy action for the Edit or context menu. */
181     private Action mCopyAction;
182 
183     /** Root of the context menu. */
184     private MenuManager mMenuManager;
185 
186     /** The view hierarchy associated with this canvas. */
187     private final ViewHierarchy mViewHierarchy = new ViewHierarchy(this);
188 
189     /** The selection in the canvas. */
190     private final SelectionManager mSelectionManager = new SelectionManager(this);
191 
192     /** The overlay which paints the optional outline. */
193     private OutlineOverlay mOutlineOverlay;
194 
195     /** The overlay which paints outlines around empty children */
196     private EmptyViewsOverlay mEmptyOverlay;
197 
198     /** The overlay which paints the mouse hover. */
199     private HoverOverlay mHoverOverlay;
200 
201     /** The overlay which paints the selection. */
202     private SelectionOverlay mSelectionOverlay;
203 
204     /** The overlay which paints the rendered layout image. */
205     private ImageOverlay mImageOverlay;
206 
207     /** The overlay which paints masks hiding everything but included content. */
208     private IncludeOverlay mIncludeOverlay;
209 
210     /**
211      * Gesture Manager responsible for identifying mouse, keyboard and drag and
212      * drop events.
213      */
214     private final GestureManager mGestureManager = new GestureManager(this);
215 
216     /**
217      * When set, performs a zoom-to-fit when the next rendering image arrives.
218      */
219     private boolean mZoomFitNextImage;
220 
221     /**
222      * Native clipboard support.
223      */
224     private ClipboardSupport mClipboardSupport;
225 
LayoutCanvas(LayoutEditorDelegate editorDelegate, RulesEngine rulesEngine, Composite parent, int style)226     public LayoutCanvas(LayoutEditorDelegate editorDelegate,
227             RulesEngine rulesEngine,
228             Composite parent,
229             int style) {
230         super(parent, style | SWT.DOUBLE_BUFFERED | SWT.V_SCROLL | SWT.H_SCROLL);
231         mEditorDelegate = editorDelegate;
232         mRulesEngine = rulesEngine;
233 
234         mClipboardSupport = new ClipboardSupport(this, parent);
235         mHScale = new CanvasTransform(this, getHorizontalBar());
236         mVScale = new CanvasTransform(this, getVerticalBar());
237 
238         // Unit test suite passes a null here; TODO: Replace with mocking
239         IFile file = editorDelegate != null ? editorDelegate.getEditor().getInputFile() : null;
240         if (file != null) {
241             String zoom = AdtPlugin.getFileProperty(file, NAME_ZOOM);
242             if (zoom != null) {
243                 try {
244                     double initialScale = Double.parseDouble(zoom);
245                     if (initialScale > 0.1) {
246                         mHScale.setScale(initialScale);
247                         mVScale.setScale(initialScale);
248                     }
249                 } catch (NumberFormatException nfe) {
250                     // Ignore - use zoom=100%
251                 }
252             } else {
253                 mZoomFitNextImage = true;
254             }
255         }
256 
257         mGCWrapper = new GCWrapper(mHScale, mVScale);
258 
259         Display display = getDisplay();
260         mFont = display.getSystemFont();
261 
262         // --- Set up graphic overlays
263         // mOutlineOverlay and mEmptyOverlay are initialized lazily
264         mHoverOverlay = new HoverOverlay(this, mHScale, mVScale);
265         mHoverOverlay.create(display);
266         mSelectionOverlay = new SelectionOverlay(this);
267         mSelectionOverlay.create(display);
268         mImageOverlay = new ImageOverlay(this, mHScale, mVScale);
269         mIncludeOverlay = new IncludeOverlay(this);
270         mImageOverlay.create(display);
271 
272         // --- Set up listeners
273         addPaintListener(new PaintListener() {
274             @Override
275             public void paintControl(PaintEvent e) {
276                 onPaint(e);
277             }
278         });
279 
280         addControlListener(new ControlAdapter() {
281             @Override
282             public void controlResized(ControlEvent e) {
283                 super.controlResized(e);
284 
285                 mHScale.setClientSize(getClientArea().width);
286                 mVScale.setClientSize(getClientArea().height);
287 
288                 // Update the zoom level in the canvas when you toggle the zoom
289                 getDisplay().asyncExec(mZoomCheck);
290             }
291         });
292 
293         // --- setup drag'n'drop ---
294         // DND Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html
295 
296         mDropTarget = createDropTarget(this);
297         mDragSource = createDragSource(this);
298         mGestureManager.registerListeners(mDragSource, mDropTarget);
299 
300         if (mEditorDelegate == null) {
301             // TODO: In another CL we should use EasyMock/objgen to provide an editor.
302             return; // Unit test
303         }
304 
305         // --- setup context menu ---
306         setupGlobalActionHandlers();
307         createContextMenu();
308 
309         // --- setup outline ---
310         // Get the outline associated with this editor, if any and of the right type.
311         if (editorDelegate != null) {
312             mOutlinePage = editorDelegate.getGraphicalOutline();
313         }
314     }
315 
316     private Runnable mZoomCheck = new Runnable() {
317         private Boolean mWasZoomed;
318 
319         @Override
320         public void run() {
321             if (isDisposed()) {
322                 return;
323             }
324 
325             IEditorPart editor = getEditorDelegate().getEditor();
326             IWorkbenchPage page = editor.getSite().getPage();
327             Boolean zoomed = page.isPageZoomed();
328             if (mWasZoomed != zoomed) {
329                 if (mWasZoomed != null) {
330                     setFitScale(true /*onlyZoomOut*/);
331                 }
332                 mWasZoomed = zoomed;
333             }
334         }
335     };
336 
handleKeyPressed(KeyEvent e)337     void handleKeyPressed(KeyEvent e) {
338         // Set up backspace as an alias for the delete action within the canvas.
339         // On most Macs there is no delete key - though there IS a key labeled
340         // "Delete" and it sends a backspace key code! In short, for Macs we should
341         // treat backspace as delete, and it's harmless (and probably useful) to
342         // handle backspace for other platforms as well.
343         if (e.keyCode == SWT.BS) {
344             mDeleteAction.run();
345         } else if (e.keyCode == SWT.ESC) {
346             mSelectionManager.selectParent();
347         } else {
348             // Zooming actions
349             char c = e.character;
350             LayoutActionBar actionBar = mEditorDelegate.getGraphicalEditor().getLayoutActionBar();
351             if (c == '1' && actionBar.isZoomingAllowed()) {
352                 setScale(1, true);
353             } else if (c == '0' && actionBar.isZoomingAllowed()) {
354                 setFitScale(true);
355             } else if (e.keyCode == '0' && (e.stateMask & SWT.MOD2) != 0
356                     && actionBar.isZoomingAllowed()) {
357                 setFitScale(false);
358             } else if (c == '+' && actionBar.isZoomingAllowed()) {
359                 actionBar.rescale(1);
360             } else if (c == '-' && actionBar.isZoomingAllowed()) {
361                 actionBar.rescale(-1);
362             }
363         }
364     }
365 
366     @Override
dispose()367     public void dispose() {
368         super.dispose();
369 
370         mGestureManager.unregisterListeners(mDragSource, mDropTarget);
371 
372         if (mDropTarget != null) {
373             mDropTarget.dispose();
374             mDropTarget = null;
375         }
376 
377         if (mRulesEngine != null) {
378             mRulesEngine.dispose();
379             mRulesEngine = null;
380         }
381 
382         if (mDragSource != null) {
383             mDragSource.dispose();
384             mDragSource = null;
385         }
386 
387         if (mClipboardSupport != null) {
388             mClipboardSupport.dispose();
389             mClipboardSupport = null;
390         }
391 
392         if (mGCWrapper != null) {
393             mGCWrapper.dispose();
394             mGCWrapper = null;
395         }
396 
397         if (mOutlineOverlay != null) {
398             mOutlineOverlay.dispose();
399             mOutlineOverlay = null;
400         }
401 
402         if (mEmptyOverlay != null) {
403             mEmptyOverlay.dispose();
404             mEmptyOverlay = null;
405         }
406 
407         if (mHoverOverlay != null) {
408             mHoverOverlay.dispose();
409             mHoverOverlay = null;
410         }
411 
412         if (mSelectionOverlay != null) {
413             mSelectionOverlay.dispose();
414             mSelectionOverlay = null;
415         }
416 
417         if (mImageOverlay != null) {
418             mImageOverlay.dispose();
419             mImageOverlay = null;
420         }
421 
422         if (mIncludeOverlay != null) {
423             mIncludeOverlay.dispose();
424             mIncludeOverlay = null;
425         }
426 
427         mViewHierarchy.dispose();
428     }
429 
430     /** Returns the Rules Engine, associated with the current project. */
getRulesEngine()431     /* package */ RulesEngine getRulesEngine() {
432         return mRulesEngine;
433     }
434 
435     /** Sets the Rules Engine, associated with the current project. */
setRulesEngine(RulesEngine rulesEngine)436     /* package */ void setRulesEngine(RulesEngine rulesEngine) {
437         mRulesEngine = rulesEngine;
438     }
439 
440     /**
441      * Returns the factory to use to convert from {@link CanvasViewInfo} or from
442      * {@link UiViewElementNode} to {@link INode} proxies.
443      */
getNodeFactory()444     /* package */ NodeFactory getNodeFactory() {
445         return mNodeFactory;
446     }
447 
448     /**
449      * Returns the GCWrapper used to paint view rules.
450      *
451      * @return The GCWrapper used to paint view rules
452      */
getGcWrapper()453     /* package */ GCWrapper getGcWrapper() {
454         return mGCWrapper;
455     }
456 
457     /**
458      * Returns the {@link LayoutEditorDelegate} associated with this canvas.
459      */
getEditorDelegate()460     public LayoutEditorDelegate getEditorDelegate() {
461         return mEditorDelegate;
462     }
463 
464     /**
465      * Returns the current {@link ImageOverlay} painting the rendered result
466      *
467      * @return the image overlay responsible for painting the rendered result, never null
468      */
getImageOverlay()469     ImageOverlay getImageOverlay() {
470         return mImageOverlay;
471     }
472 
473     /**
474      * Returns the current {@link SelectionOverlay} painting the selection highlights
475      *
476      * @return the selection overlay responsible for painting the selection highlights,
477      *         never null
478      */
getSelectionOverlay()479     SelectionOverlay getSelectionOverlay() {
480         return mSelectionOverlay;
481     }
482 
483     /**
484      * Returns the {@link GestureManager} associated with this canvas.
485      *
486      * @return the {@link GestureManager} associated with this canvas, never null.
487      */
getGestureManager()488     GestureManager getGestureManager() {
489         return mGestureManager;
490     }
491 
492     /**
493      * Returns the current {@link HoverOverlay} painting the mouse hover.
494      *
495      * @return the hover overlay responsible for painting the mouse hover,
496      *         never null
497      */
getHoverOverlay()498     HoverOverlay getHoverOverlay() {
499         return mHoverOverlay;
500     }
501 
502     /**
503      * Returns the horizontal {@link CanvasTransform} transform object, which can map
504      * a layout point into a control point.
505      *
506      * @return A {@link CanvasTransform} for mapping between layout and control
507      *         coordinates in the horizontal dimension.
508      */
getHorizontalTransform()509     /* package */ CanvasTransform getHorizontalTransform() {
510         return mHScale;
511     }
512 
513     /**
514      * Returns the vertical {@link CanvasTransform} transform object, which can map a
515      * layout point into a control point.
516      *
517      * @return A {@link CanvasTransform} for mapping between layout and control
518      *         coordinates in the vertical dimension.
519      */
getVerticalTransform()520     /* package */ CanvasTransform getVerticalTransform() {
521         return mVScale;
522     }
523 
524     /**
525      * Returns the {@link OutlinePage} associated with this canvas
526      *
527      * @return the {@link OutlinePage} associated with this canvas
528      */
getOutlinePage()529     public OutlinePage getOutlinePage() {
530         return mOutlinePage;
531     }
532 
533     /**
534      * Returns the {@link SelectionManager} associated with this canvas.
535      *
536      * @return The {@link SelectionManager} holding the selection for this
537      *         canvas. Never null.
538      */
getSelectionManager()539     public SelectionManager getSelectionManager() {
540         return mSelectionManager;
541     }
542 
543     /**
544      * Returns the {@link ViewHierarchy} object associated with this canvas,
545      * holding the most recent rendered view of the scene, if valid.
546      *
547      * @return The {@link ViewHierarchy} object associated with this canvas.
548      *         Never null.
549      */
getViewHierarchy()550     public ViewHierarchy getViewHierarchy() {
551         return mViewHierarchy;
552     }
553 
554     /**
555      * Returns the {@link ClipboardSupport} object associated with this canvas.
556      *
557      * @return The {@link ClipboardSupport} object for this canvas. Null only after dispose.
558      */
getClipboardSupport()559     public ClipboardSupport getClipboardSupport() {
560         return mClipboardSupport;
561     }
562 
563     /** Returns the Select All action bound to this canvas */
getSelectAllAction()564     Action getSelectAllAction() {
565         return mSelectAllAction;
566     }
567 
568     /**
569      * Sets the result of the layout rendering. The result object indicates if the layout
570      * rendering succeeded. If it did, it contains a bitmap and the objects rectangles.
571      *
572      * Implementation detail: the bridge's computeLayout() method already returns a newly
573      * allocated ILayourResult. That means we can keep this result and hold on to it
574      * when it is valid.
575      *
576      * @param session The new scene, either valid or not.
577      * @param explodedNodes The set of individual nodes the layout computer was asked to
578      *            explode. Note that these are independent of the explode-all mode where
579      *            all views are exploded; this is used only for the mode (
580      *            {@link #showInvisibleViews(boolean)}) where individual invisible nodes
581      *            are padded during certain interactions.
582      */
setSession(RenderSession session, Set<UiElementNode> explodedNodes, boolean layoutlib5)583     /* package */ void setSession(RenderSession session, Set<UiElementNode> explodedNodes,
584             boolean layoutlib5) {
585         // disable any hover
586         clearHover();
587 
588         mViewHierarchy.setSession(session, explodedNodes, layoutlib5);
589         if (mViewHierarchy.isValid() && session != null) {
590             Image image = mImageOverlay.setImage(session.getImage(), session.isAlphaChannelImage());
591 
592             mOutlinePage.setModel(mViewHierarchy.getRoot());
593             mEditorDelegate.getGraphicalEditor().setModel(mViewHierarchy.getRoot());
594 
595             if (image != null) {
596                 mHScale.setSize(image.getImageData().width, getClientArea().width);
597                 mVScale.setSize(image.getImageData().height, getClientArea().height);
598                 if (mZoomFitNextImage) {
599                     mZoomFitNextImage = false;
600                     // Must be run asynchronously because getClientArea() returns 0 bounds
601                     // when the editor is being initialized
602                     getDisplay().asyncExec(new Runnable() {
603                         @Override
604                         public void run() {
605                             setFitScale(true);
606                         }
607                     });
608                 }
609             }
610         }
611 
612         redraw();
613     }
614 
setShowOutline(boolean newState)615     /* package */ void setShowOutline(boolean newState) {
616         mShowOutline = newState;
617         redraw();
618     }
619 
getScale()620     public double getScale() {
621         return mHScale.getScale();
622     }
623 
setScale(double scale, boolean redraw)624     /* package */ void setScale(double scale, boolean redraw) {
625         if (scale <= 0.0) {
626             scale = 1.0;
627         }
628 
629         if (scale == getScale()) {
630             return;
631         }
632 
633         mHScale.setScale(scale);
634         mVScale.setScale(scale);
635         if (redraw) {
636             redraw();
637         }
638 
639         // Clear the zoom setting if it is almost identical to 1.0
640         String zoomValue = (Math.abs(scale - 1.0) < 0.0001) ? null : Double.toString(scale);
641         IFile file = mEditorDelegate.getEditor().getInputFile();
642         if (file != null) {
643             AdtPlugin.setFileProperty(file, NAME_ZOOM, zoomValue);
644         }
645     }
646 
647     /**
648      * Scales the canvas to best fit
649      *
650      * @param onlyZoomOut if true, then the zooming factor will never be larger than 1,
651      *            which means that this function will zoom out if necessary to show the
652      *            rendered image, but it will never zoom in.
653      */
setFitScale(boolean onlyZoomOut)654     void setFitScale(boolean onlyZoomOut) {
655         Image image = getImageOverlay().getImage();
656         if (image != null) {
657             Rectangle canvasSize = getClientArea();
658             int canvasWidth = canvasSize.width;
659             int canvasHeight = canvasSize.height;
660 
661             ImageData imageData = image.getImageData();
662             int sceneWidth = imageData.width;
663             int sceneHeight = imageData.height;
664             if (sceneWidth == 0.0 || sceneHeight == 0.0) {
665                 return;
666             }
667 
668             // Reduce the margins if necessary
669             int hDelta = canvasWidth - sceneWidth;
670             int hMargin = 0;
671             if (hDelta > 2 * CanvasTransform.DEFAULT_MARGIN) {
672                 hMargin = CanvasTransform.DEFAULT_MARGIN;
673             } else if (hDelta > 0) {
674                 hMargin = hDelta / 2;
675             }
676 
677             int vDelta = canvasHeight - sceneHeight;
678             int vMargin = 0;
679             if (vDelta > 2 * CanvasTransform.DEFAULT_MARGIN) {
680                 vMargin = CanvasTransform.DEFAULT_MARGIN;
681             } else if (vDelta > 0) {
682                 vMargin = vDelta / 2;
683             }
684 
685             double hScale = (canvasWidth - 2 * hMargin) / (double) sceneWidth;
686             double vScale = (canvasHeight - 2 * vMargin) / (double) sceneHeight;
687 
688             double scale = Math.min(hScale, vScale);
689 
690             if (onlyZoomOut) {
691                 scale = Math.min(1.0, scale);
692             }
693 
694             setScale(scale, true);
695         }
696     }
697 
698     /**
699      * Transforms a point, expressed in layout coordinates, into "client" coordinates
700      * relative to the control (and not relative to the display).
701      *
702      * @param canvasX X in the canvas coordinates
703      * @param canvasY Y in the canvas coordinates
704      * @return A new {@link Point} in control client coordinates (not display coordinates)
705      */
layoutToControlPoint(int canvasX, int canvasY)706     /* package */ Point layoutToControlPoint(int canvasX, int canvasY) {
707         int x = mHScale.translate(canvasX);
708         int y = mVScale.translate(canvasY);
709         return new Point(x, y);
710     }
711 
712     /**
713      * Returns the action for the context menu corresponding to the given action id.
714      * <p/>
715      * For global actions such as copy or paste, the action id must be composed of
716      * the {@link #PREFIX_CANVAS_ACTION} followed by one of {@link ActionFactory}'s
717      * action ids.
718      * <p/>
719      * Returns null if there's no action for the given id.
720      */
getAction(String actionId)721     /* package */ IAction getAction(String actionId) {
722         String prefix = PREFIX_CANVAS_ACTION;
723         if (mMenuManager == null ||
724                 actionId == null ||
725                 !actionId.startsWith(prefix)) {
726             return null;
727         }
728 
729         actionId = actionId.substring(prefix.length());
730 
731         for (IContributionItem contrib : mMenuManager.getItems()) {
732             if (contrib instanceof ActionContributionItem &&
733                     actionId.equals(contrib.getId())) {
734                 return ((ActionContributionItem) contrib).getAction();
735             }
736         }
737 
738         return null;
739     }
740 
741     //---------------
742 
743     /**
744      * Paints the canvas in response to paint events.
745      */
onPaint(PaintEvent e)746     private void onPaint(PaintEvent e) {
747         GC gc = e.gc;
748         gc.setFont(mFont);
749         mGCWrapper.setGC(gc);
750         try {
751             if (!mImageOverlay.isHiding()) {
752                 mImageOverlay.paint(gc);
753             }
754 
755             if (mShowOutline) {
756                 if (mOutlineOverlay == null) {
757                     mOutlineOverlay = new OutlineOverlay(mViewHierarchy, mHScale, mVScale);
758                     mOutlineOverlay.create(getDisplay());
759                 }
760                 if (!mOutlineOverlay.isHiding()) {
761                     mOutlineOverlay.paint(gc);
762                 }
763             }
764 
765             if (mShowInvisible) {
766                 if (mEmptyOverlay == null) {
767                     mEmptyOverlay = new EmptyViewsOverlay(mViewHierarchy, mHScale, mVScale);
768                     mEmptyOverlay.create(getDisplay());
769                 }
770                 if (!mEmptyOverlay.isHiding()) {
771                     mEmptyOverlay.paint(gc);
772                 }
773             }
774 
775             if (!mHoverOverlay.isHiding()) {
776                 mHoverOverlay.paint(gc);
777             }
778             if (!mIncludeOverlay.isHiding()) {
779                 mIncludeOverlay.paint(gc);
780             }
781 
782             if (!mSelectionOverlay.isHiding()) {
783                 mSelectionOverlay.paint(mSelectionManager, mGCWrapper, gc, mRulesEngine);
784             }
785             mGestureManager.paint(gc);
786 
787         } finally {
788             mGCWrapper.setGC(null);
789         }
790     }
791 
792     /**
793      * Shows or hides invisible parent views, which are views which have empty bounds and
794      * no children. The nodes which will be shown are provided by
795      * {@link #getNodesToExplode()}.
796      *
797      * @param show When true, any invisible parent nodes are padded and highlighted
798      *            ("exploded"), and when false any formerly exploded nodes are hidden.
799      */
showInvisibleViews(boolean show)800     /* package */ void showInvisibleViews(boolean show) {
801         if (mShowInvisible == show) {
802             return;
803         }
804         mShowInvisible = show;
805 
806         // Optimization: Avoid doing work when we don't have invisible parents (on show)
807         // or formerly exploded nodes (on hide).
808         if (show && !mViewHierarchy.hasInvisibleParents()) {
809             return;
810         } else if (!show && !mViewHierarchy.hasExplodedParents()) {
811             return;
812         }
813 
814         mEditorDelegate.recomputeLayout();
815     }
816 
817     /**
818      * Returns a set of nodes that should be exploded (forced non-zero padding during render),
819      * or null if no nodes should be exploded. (Note that this is independent of the
820      * explode-all mode, where all nodes are padded -- that facility does not use this
821      * mechanism, which is only intended to be used to expose invisible parent nodes.
822      *
823      * @return The set of invisible parents, or null if no views should be expanded.
824      */
getNodesToExplode()825     public Set<UiElementNode> getNodesToExplode() {
826         if (mShowInvisible) {
827             return mViewHierarchy.getInvisibleNodes();
828         }
829 
830         // IF we have selection, and IF we have invisible nodes in the view,
831         // see if any of the selected items are among the invisible nodes, and if so
832         // add them to a lazily constructed set which we pass back for rendering.
833         Set<UiElementNode> result = null;
834         List<SelectionItem> selections = mSelectionManager.getSelections();
835         if (selections.size() > 0) {
836             List<CanvasViewInfo> invisibleParents = mViewHierarchy.getInvisibleViews();
837             if (invisibleParents.size() > 0) {
838                 for (SelectionItem item : selections) {
839                     CanvasViewInfo viewInfo = item.getViewInfo();
840                     // O(n^2) here, but both the selection size and especially the
841                     // invisibleParents size are expected to be small
842                     if (invisibleParents.contains(viewInfo)) {
843                         UiViewElementNode node = viewInfo.getUiViewNode();
844                         if (node != null) {
845                             if (result == null) {
846                                 result = new HashSet<UiElementNode>();
847                             }
848                             result.add(node);
849                         }
850                     }
851                 }
852             }
853         }
854 
855         return result;
856     }
857 
858     /**
859      * Clears the hover.
860      */
clearHover()861     /* package */ void clearHover() {
862         mHoverOverlay.clearHover();
863     }
864 
865     /**
866      * Hover on top of a known child.
867      */
hover(MouseEvent e)868     /* package */ void hover(MouseEvent e) {
869         // Check if a button is pressed; no hovers during drags
870         if ((e.stateMask & SWT.BUTTON_MASK) != 0) {
871             clearHover();
872             return;
873         }
874 
875         LayoutPoint p = ControlPoint.create(this, e).toLayout();
876         CanvasViewInfo vi = mViewHierarchy.findViewInfoAt(p);
877 
878         // We don't hover on the root since it's not a widget per see and it is always there.
879         // We also skip spacers...
880         if (vi != null && (vi.isRoot() || vi.isHidden())) {
881             vi = null;
882         }
883 
884         boolean needsUpdate = vi != mHoverViewInfo;
885         mHoverViewInfo = vi;
886 
887         if (vi == null) {
888             clearHover();
889         } else {
890             Rectangle r = vi.getSelectionRect();
891             mHoverOverlay.setHover(r.x, r.y, r.width, r.height);
892         }
893 
894         if (needsUpdate) {
895             redraw();
896         }
897     }
898 
899     /**
900      * Shows the given {@link CanvasViewInfo}, which can mean exposing its XML or if it's
901      * an included element, its corresponding file.
902      *
903      * @param vi the {@link CanvasViewInfo} to be shown
904      */
show(CanvasViewInfo vi)905     public void show(CanvasViewInfo vi) {
906         String url = vi.getIncludeUrl();
907         if (url != null) {
908             showInclude(url);
909         } else {
910             showXml(vi);
911         }
912     }
913 
914     /**
915      * Shows the layout file referenced by the given url in the same project.
916      *
917      * @param url The layout attribute url of the form @layout/foo
918      */
showInclude(String url)919     private void showInclude(String url) {
920         GraphicalEditorPart graphicalEditor = mEditorDelegate.getGraphicalEditor();
921         IPath filePath = graphicalEditor.findResourceFile(url);
922         if (filePath == null) {
923             // Should not be possible - if the URL had been bad, then we wouldn't
924             // have been able to render the scene and you wouldn't have been able
925             // to click on it
926             return;
927         }
928 
929         // Save the including file, if necessary: without it, the "Show Included In"
930         // facility which is invoked automatically will not work properly if the <include>
931         // tag is not in the saved version of the file, since the outer file is read from
932         // disk rather than from memory.
933         IEditorSite editorSite = graphicalEditor.getEditorSite();
934         IWorkbenchPage page = editorSite.getPage();
935         page.saveEditor(mEditorDelegate.getEditor(), false);
936 
937         IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
938         IFile xmlFile = null;
939         IPath workspacePath = workspace.getLocation();
940         if (workspacePath.isPrefixOf(filePath)) {
941             IPath relativePath = filePath.makeRelativeTo(workspacePath);
942             xmlFile = (IFile) workspace.findMember(relativePath);
943         } else if (filePath.isAbsolute()) {
944             xmlFile = workspace.getFileForLocation(filePath);
945         }
946         if (xmlFile != null) {
947             IFile leavingFile = graphicalEditor.getEditedFile();
948             Reference next = Reference.create(graphicalEditor.getEditedFile());
949 
950             try {
951                 IEditorPart openAlready = EditorUtility.isOpenInEditor(xmlFile);
952 
953                 // Show the included file as included within this click source?
954                 if (openAlready != null) {
955                     LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(openAlready);
956                     if (delegate != null) {
957                         GraphicalEditorPart gEditor = delegate.getGraphicalEditor();
958                         if (gEditor != null &&
959                                 gEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) {
960                             gEditor.showIn(next);
961                         }
962                     }
963                 } else {
964                     try {
965                         // Set initial state of a new file
966                         // TODO: Only set rendering target portion of the state
967                         QualifiedName qname = ConfigurationComposite.NAME_CONFIG_STATE;
968                         String state = AdtPlugin.getFileProperty(leavingFile, qname);
969                         xmlFile.setSessionProperty(GraphicalEditorPart.NAME_INITIAL_STATE,
970                                 state);
971                     } catch (CoreException e) {
972                         // pass
973                     }
974 
975                     if (graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) {
976                         try {
977                             xmlFile.setSessionProperty(GraphicalEditorPart.NAME_INCLUDE, next);
978                         } catch (CoreException e) {
979                             // pass - worst that can happen is that we don't
980                             //start with inclusion
981                         }
982                     }
983                 }
984 
985                 EditorUtility.openInEditor(xmlFile, true);
986                 return;
987             } catch (PartInitException ex) {
988                 AdtPlugin.log(ex, "Can't open %$1s", url); //$NON-NLS-1$
989             }
990         } else {
991             // It's not a path in the workspace; look externally
992             // (this is probably an @android: path)
993             if (filePath.isAbsolute()) {
994                 IFileStore fileStore = EFS.getLocalFileSystem().getStore(filePath);
995                 // fileStore = fileStore.getChild(names[i]);
996                 if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) {
997                     try {
998                         IDE.openEditorOnFileStore(page, fileStore);
999                         return;
1000                     } catch (PartInitException ex) {
1001                         AdtPlugin.log(ex, "Can't open %$1s", url); //$NON-NLS-1$
1002                     }
1003                 }
1004             }
1005         }
1006 
1007         // Failed: display message to the user
1008         String message = String.format("Could not find resource %1$s", url);
1009         IStatusLineManager status = editorSite.getActionBars().getStatusLineManager();
1010         status.setErrorMessage(message);
1011         getDisplay().beep();
1012     }
1013 
1014     /**
1015      * Returns the layout resource name of this layout
1016      *
1017      * @return the layout resource name of this layout
1018      */
getLayoutResourceName()1019     public String getLayoutResourceName() {
1020         GraphicalEditorPart graphicalEditor = mEditorDelegate.getGraphicalEditor();
1021         return graphicalEditor.getLayoutResourceName();
1022     }
1023 
1024     /**
1025      * Returns the layout resource url of the current layout
1026      *
1027      * @return
1028      */
1029     /*
1030     public String getMe() {
1031         GraphicalEditorPart graphicalEditor = mEditorDelegate.getGraphicalEditor();
1032         IFile editedFile = graphicalEditor.getEditedFile();
1033         return editedFile.getProjectRelativePath().toOSString();
1034     }
1035      */
1036 
1037     /**
1038      * Show the XML element corresponding to the given {@link CanvasViewInfo} (unless it's
1039      * a root).
1040      *
1041      * @param vi The clicked {@link CanvasViewInfo} whose underlying XML element we want
1042      *            to view
1043      */
showXml(CanvasViewInfo vi)1044     private void showXml(CanvasViewInfo vi) {
1045         // Warp to the text editor and show the corresponding XML for the
1046         // double-clicked widget
1047         if (vi.isRoot()) {
1048             return;
1049         }
1050 
1051         Node xmlNode = vi.getXmlNode();
1052         if (xmlNode != null) {
1053             boolean found = mEditorDelegate.getEditor().show(xmlNode);
1054             if (!found) {
1055                 getDisplay().beep();
1056             }
1057         }
1058     }
1059 
1060     //---------------
1061 
1062     /**
1063      * Helper to create the drag source for the given control.
1064      * <p/>
1065      * This is static with package-access so that {@link OutlinePage} can also
1066      * create an exact copy of the source with the same attributes.
1067      */
createDragSource(Control control)1068     /* package */static DragSource createDragSource(Control control) {
1069         DragSource source = new DragSource(control, DND.DROP_COPY | DND.DROP_MOVE);
1070         source.setTransfer(new Transfer[] {
1071                 TextTransfer.getInstance(),
1072                 SimpleXmlTransfer.getInstance()
1073         });
1074         return source;
1075     }
1076 
1077     /**
1078      * Helper to create the drop target for the given control.
1079      */
createDropTarget(Control control)1080     private static DropTarget createDropTarget(Control control) {
1081         DropTarget dropTarget = new DropTarget(
1082                 control, DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT);
1083         dropTarget.setTransfer(new Transfer[] {
1084             SimpleXmlTransfer.getInstance()
1085         });
1086         return dropTarget;
1087     }
1088 
1089     //---------------
1090 
1091     /**
1092      * Invoked by the constructor to add our cut/copy/paste/delete/select-all
1093      * handlers in the global action handlers of this editor's site.
1094      * <p/>
1095      * This will enable the menu items under the global Edit menu and make them
1096      * invoke our actions as needed. As a benefit, the corresponding shortcut
1097      * accelerators will do what one would expect.
1098      */
setupGlobalActionHandlers()1099     private void setupGlobalActionHandlers() {
1100         mCutAction = new Action() {
1101             @Override
1102             public void run() {
1103                 mClipboardSupport.cutSelectionToClipboard(mSelectionManager.getSnapshot());
1104                 updateMenuActionState();
1105             }
1106         };
1107 
1108         copyActionAttributes(mCutAction, ActionFactory.CUT);
1109 
1110         mCopyAction = new Action() {
1111             @Override
1112             public void run() {
1113                 mClipboardSupport.copySelectionToClipboard(mSelectionManager.getSnapshot());
1114                 updateMenuActionState();
1115             }
1116         };
1117 
1118         copyActionAttributes(mCopyAction, ActionFactory.COPY);
1119 
1120         mPasteAction = new Action() {
1121             @Override
1122             public void run() {
1123                 mClipboardSupport.pasteSelection(mSelectionManager.getSnapshot());
1124                 updateMenuActionState();
1125             }
1126         };
1127 
1128         copyActionAttributes(mPasteAction, ActionFactory.PASTE);
1129 
1130         mDeleteAction = new Action() {
1131             @Override
1132             public void run() {
1133                 mClipboardSupport.deleteSelection(
1134                         getDeleteLabel(),
1135                         mSelectionManager.getSnapshot());
1136             }
1137         };
1138 
1139         copyActionAttributes(mDeleteAction, ActionFactory.DELETE);
1140 
1141         mSelectAllAction = new Action() {
1142             @Override
1143             public void run() {
1144                 GraphicalEditorPart graphicalEditor = getEditorDelegate().getGraphicalEditor();
1145                 StyledText errorLabel = graphicalEditor.getErrorLabel();
1146                 if (errorLabel.isFocusControl()) {
1147                     errorLabel.selectAll();
1148                     return;
1149                 }
1150 
1151                 mSelectionManager.selectAll();
1152             }
1153         };
1154 
1155         copyActionAttributes(mSelectAllAction, ActionFactory.SELECT_ALL);
1156     }
1157 
getCutLabel()1158     /* package */ String getCutLabel() {
1159         return mCutAction.getText();
1160     }
1161 
getDeleteLabel()1162     /* package */ String getDeleteLabel() {
1163         // verb "Delete" from the DELETE action's title
1164         return mDeleteAction.getText();
1165     }
1166 
1167     /**
1168      * Updates menu actions that depends on the selection.
1169      */
updateMenuActionState()1170     void updateMenuActionState() {
1171         List<SelectionItem> selections = getSelectionManager().getSelections();
1172         boolean hasSelection = !selections.isEmpty();
1173         if (hasSelection && selections.size() == 1 && selections.get(0).isRoot()) {
1174             hasSelection = false;
1175         }
1176 
1177         StyledText errorLabel = mEditorDelegate.getGraphicalEditor().getErrorLabel();
1178         mCutAction.setEnabled(hasSelection);
1179         mCopyAction.setEnabled(hasSelection || errorLabel.getSelectionCount() > 0);
1180         mDeleteAction.setEnabled(hasSelection);
1181         // Select All should *always* be selectable, regardless of whether anything
1182         // is currently selected.
1183         mSelectAllAction.setEnabled(true);
1184 
1185         // The paste operation is only available if we can paste our custom type.
1186         // We do not currently support pasting random text (e.g. XML). Maybe later.
1187         boolean hasSxt = mClipboardSupport.hasSxtOnClipboard();
1188         mPasteAction.setEnabled(hasSxt);
1189     }
1190 
1191     /**
1192      * Update the actions when this editor is activated
1193      *
1194      * @param bars the action bar for this canvas
1195      */
updateGlobalActions(IActionBars bars)1196     public void updateGlobalActions(IActionBars bars) {
1197         updateMenuActionState();
1198         assert bars != null;
1199         bars.setGlobalActionHandler(ActionFactory.CUT.getId(), mCutAction);
1200         bars.setGlobalActionHandler(ActionFactory.COPY.getId(), mCopyAction);
1201         bars.setGlobalActionHandler(ActionFactory.PASTE.getId(), mPasteAction);
1202         bars.setGlobalActionHandler(ActionFactory.DELETE.getId(), mDeleteAction);
1203         bars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), mSelectAllAction);
1204 
1205         ITextEditor editor = mEditorDelegate.getEditor().getStructuredTextEditor();
1206         IAction undoAction = editor.getAction(ActionFactory.UNDO.getId());
1207         bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), undoAction);
1208         IAction redoAction = editor.getAction(ActionFactory.REDO.getId());
1209         bars.setGlobalActionHandler(ActionFactory.REDO.getId(), redoAction);
1210 
1211         bars.updateActionBars();
1212     }
1213 
1214     /**
1215      * Helper for {@link #setupGlobalActionHandlers()}.
1216      * Copies the action attributes form the given {@link ActionFactory}'s action to
1217      * our action.
1218      * <p/>
1219      * {@link ActionFactory} provides access to the standard global actions in Eclipse.
1220      * <p/>
1221      * This allows us to grab the standard labels and icons for the
1222      * global actions such as copy, cut, paste, delete and select-all.
1223      */
copyActionAttributes(Action action, ActionFactory factory)1224     private void copyActionAttributes(Action action, ActionFactory factory) {
1225         IWorkbenchAction wa = factory.create(
1226                 mEditorDelegate.getEditor().getEditorSite().getWorkbenchWindow());
1227         action.setId(wa.getId());
1228         action.setText(wa.getText());
1229         action.setEnabled(wa.isEnabled());
1230         action.setDescription(wa.getDescription());
1231         action.setToolTipText(wa.getToolTipText());
1232         action.setAccelerator(wa.getAccelerator());
1233         action.setActionDefinitionId(wa.getActionDefinitionId());
1234         action.setImageDescriptor(wa.getImageDescriptor());
1235         action.setHoverImageDescriptor(wa.getHoverImageDescriptor());
1236         action.setDisabledImageDescriptor(wa.getDisabledImageDescriptor());
1237         action.setHelpListener(wa.getHelpListener());
1238     }
1239 
1240     /**
1241      * Creates the context menu for the canvas. This is called once from the canvas' constructor.
1242      * <p/>
1243      * The menu has a static part with actions that are always available such as
1244      * copy, cut, paste and show in > explorer. This is created by
1245      * {@link #setupStaticMenuActions(IMenuManager)}.
1246      * <p/>
1247      * There's also a dynamic part that is populated by the rules of the
1248      * selected elements, created by {@link DynamicContextMenu}.
1249      */
1250     @SuppressWarnings("unused")
createContextMenu()1251     private void createContextMenu() {
1252 
1253         // This manager is the root of the context menu.
1254         mMenuManager = new MenuManager() {
1255             @Override
1256             public boolean isDynamic() {
1257                 return true;
1258             }
1259         };
1260 
1261         // Fill the menu manager with the static & dynamic actions
1262         setupStaticMenuActions(mMenuManager);
1263         new DynamicContextMenu(mEditorDelegate, this, mMenuManager);
1264         Menu menu = mMenuManager.createContextMenu(this);
1265         setMenu(menu);
1266 
1267         // Add listener to detect when the menu is about to be posted, such that
1268         // we can sync the selection. Without this, you can right click on something
1269         // in the canvas which is NOT selected, and the context menu will show items related
1270         // to the selection, NOT the item you clicked on!!
1271         addMenuDetectListener(new MenuDetectListener() {
1272             @Override
1273             public void menuDetected(MenuDetectEvent e) {
1274                 mSelectionManager.menuClick(e);
1275             }
1276         });
1277     }
1278 
1279     /**
1280      * Invoked by {@link #createContextMenu()} to create our *static* context menu once.
1281      * <p/>
1282      * The content of the menu itself does not change. However the state of the
1283      * various items is controlled by their associated actions.
1284      * <p/>
1285      * For cut/copy/paste/delete/select-all, we explicitly reuse the actions
1286      * created by {@link #setupGlobalActionHandlers()}, so this method must be
1287      * invoked after that one.
1288      */
setupStaticMenuActions(IMenuManager manager)1289     private void setupStaticMenuActions(IMenuManager manager) {
1290         manager.removeAll();
1291 
1292         manager.add(new SelectionManager.SelectionMenu(mEditorDelegate.getGraphicalEditor()));
1293         manager.add(new Separator());
1294         manager.add(mCutAction);
1295         manager.add(mCopyAction);
1296         manager.add(mPasteAction);
1297         manager.add(new Separator());
1298         manager.add(mDeleteAction);
1299         manager.add(new Separator());
1300         manager.add(new PlayAnimationMenu(this));
1301         manager.add(new ExportScreenshotAction(this));
1302         manager.add(new Separator());
1303 
1304         // Group "Show Included In" and "Show In" together
1305         manager.add(new ShowWithinMenu(mEditorDelegate));
1306 
1307         // Create a "Show In" sub-menu and automatically populate it using standard
1308         // actions contributed by the workbench.
1309         String showInLabel = IDEWorkbenchMessages.Workbench_showIn;
1310         MenuManager showInSubMenu = new MenuManager(showInLabel);
1311         showInSubMenu.add(
1312                 ContributionItemFactory.VIEWS_SHOW_IN.create(
1313                         mEditorDelegate.getEditor().getSite().getWorkbenchWindow()));
1314         manager.add(showInSubMenu);
1315     }
1316 
1317     /**
1318      * Deletes the selection. Equivalent to pressing the Delete key.
1319      */
delete()1320     /* package */ void delete() {
1321         mDeleteAction.run();
1322     }
1323 
1324     /**
1325      * Add new root in an existing empty XML layout.
1326      * <p/>
1327      * In case of error (unknown FQCN, document not empty), silently do nothing.
1328      * In case of success, the new element will have some default attributes set
1329      * (xmlns:android, layout_width and height). The edit is wrapped in a proper
1330      * undo.
1331      * <p/>
1332      * This is invoked by
1333      * {@link MoveGesture#drop(org.eclipse.swt.dnd.DropTargetEvent)}.
1334      *
1335      * @param rootFqcn A non-null non-empty FQCN that must match an existing
1336      *            {@link ViewElementDescriptor} to add as root to the current
1337      *            empty XML document.
1338      */
createDocumentRoot(String rootFqcn)1339     /* package */ void createDocumentRoot(String rootFqcn) {
1340 
1341         // Need a valid empty document to create the new root
1342         final UiDocumentNode uiDoc = mEditorDelegate.getUiRootNode();
1343         if (uiDoc == null || uiDoc.getUiChildren().size() > 0) {
1344             debugPrintf("Failed to create document root for %1$s: document is not empty", rootFqcn);
1345             return;
1346         }
1347 
1348         // Find the view descriptor matching our FQCN
1349         final ViewElementDescriptor viewDesc = mEditorDelegate.getFqcnViewDescriptor(rootFqcn);
1350         if (viewDesc == null) {
1351             // TODO this could happen if dropping a custom view not known in this project
1352             debugPrintf("Failed to add document root, unknown FQCN %1$s", rootFqcn);
1353             return;
1354         }
1355 
1356         // Get the last segment of the FQCN for the undo title
1357         String title = rootFqcn;
1358         int pos = title.lastIndexOf('.');
1359         if (pos > 0 && pos < title.length() - 1) {
1360             title = title.substring(pos + 1);
1361         }
1362         title = String.format("Create root %1$s in document", title);
1363 
1364         mEditorDelegate.getEditor().wrapUndoEditXmlModel(title, new Runnable() {
1365             @Override
1366             public void run() {
1367                 UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc);
1368 
1369                 // A root node requires the Android XMLNS
1370                 uiNew.setAttributeValue(
1371                         LayoutConstants.ANDROID_NS_NAME,
1372                         XmlnsAttributeDescriptor.XMLNS_URI,
1373                         SdkConstants.NS_RESOURCES,
1374                         true /*override*/);
1375 
1376                 // Adjust the attributes
1377                 DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
1378 
1379                 uiNew.createXmlNode();
1380             }
1381         });
1382     }
1383 
1384     /**
1385      * Returns the insets associated with views of the given fully qualified name, for the
1386      * current theme and screen type.
1387      *
1388      * @param fqcn the fully qualified name to the widget type
1389      * @return the insets, or null if unknown
1390      */
getInsets(String fqcn)1391     public Margins getInsets(String fqcn) {
1392         if (ViewMetadataRepository.INSETS_SUPPORTED) {
1393             ConfigurationComposite configComposite =
1394                     mEditorDelegate.getGraphicalEditor().getConfigurationComposite();
1395             String theme = configComposite.getTheme();
1396             Density density = configComposite.getDensity();
1397             return ViewMetadataRepository.getInsets(fqcn, density, theme);
1398         } else {
1399             return null;
1400         }
1401     }
1402 
debugPrintf(String message, Object... params)1403     private void debugPrintf(String message, Object... params) {
1404         if (DEBUG) {
1405             AdtPlugin.printToConsole("Canvas", String.format(message, params));
1406         }
1407     }
1408 }
1409