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