Home | History | Annotate | Download | only in gle2
      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 
    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 
    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
    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. */
    431     /* package */ RulesEngine getRulesEngine() {
    432         return mRulesEngine;
    433     }
    434 
    435     /** Sets the Rules Engine, associated with the current project. */
    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      */
    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      */
    453     /* package */ GCWrapper getGcWrapper() {
    454         return mGCWrapper;
    455     }
    456 
    457     /**
    458      * Returns the {@link LayoutEditorDelegate} associated with this canvas.
    459      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    559     public ClipboardSupport getClipboardSupport() {
    560         return mClipboardSupport;
    561     }
    562 
    563     /** Returns the Select All action bound to this canvas */
    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      */
    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 
    615     /* package */ void setShowOutline(boolean newState) {
    616         mShowOutline = newState;
    617         redraw();
    618     }
    619 
    620     public double getScale() {
    621         return mHScale.getScale();
    622     }
    623 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    861     /* package */ void clearHover() {
    862         mHoverOverlay.clearHover();
    863     }
    864 
    865     /**
    866      * Hover on top of a known child.
    867      */
    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      */
    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      */
    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      */
   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      */
   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      */
   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      */
   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      */
   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 
   1158     /* package */ String getCutLabel() {
   1159         return mCutAction.getText();
   1160     }
   1161 
   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      */
   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      */
   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      */
   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")
   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      */
   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      */
   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      */
   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      */
   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 
   1403     private void debugPrintf(String message, Object... params) {
   1404         if (DEBUG) {
   1405             AdtPlugin.printToConsole("Canvas", String.format(message, params));
   1406         }
   1407     }
   1408 }
   1409