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