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      */
LayoutCanvas(LayoutEditorDelegate editorDelegate, RulesEngine rulesEngine, Composite parent, int style)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 
updateScrollBars()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 
handleKeyPressed(KeyEvent e)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
dispose()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
getPreviewManager()586     public RenderPreviewManager getPreviewManager() {
587         return mPreviewManager;
588     }
589 
590     /** Returns the Rules Engine, associated with the current project. */
getRulesEngine()591     RulesEngine getRulesEngine() {
592         return mRulesEngine;
593     }
594 
595     /** Sets the Rules Engine, associated with the current project. */
setRulesEngine(RulesEngine rulesEngine)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
getNodeFactory()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      */
getGcWrapper()616     GCWrapper getGcWrapper() {
617         return mGCWrapper;
618     }
619 
620     /**
621      * Returns the {@link LayoutEditorDelegate} associated with this canvas.
622      *
623      * @return the delegate
624      */
getEditorDelegate()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      */
getImageOverlay()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      */
getSelectionOverlay()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      */
getGestureManager()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      */
getHoverOverlay()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      */
getHorizontalTransform()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      */
getVerticalTransform()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      */
getOutlinePage()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      */
getSelectionManager()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      */
getViewHierarchy()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      */
getClipboardSupport()724     public ClipboardSupport getClipboardSupport() {
725         return mClipboardSupport;
726     }
727 
728     /** Returns the Select All action bound to this canvas */
getSelectAllAction()729     Action getSelectAllAction() {
730         return mSelectAllAction;
731     }
732 
733     /** Returns the associated {@link GraphicalEditorPart} */
getGraphicalEditor()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      */
setSession(RenderSession session, Set<UiElementNode> explodedNodes, boolean layoutlib5)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 
ensureZoomed()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 
setShowOutline(boolean newState)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      */
getScale()811     public double getScale() {
812         return mHScale.getScale();
813     }
814 
setScale(double scale, boolean redraw)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      */
setFitScale(boolean onlyZoomOut, boolean allowZoomIn)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      */
layoutToControlPoint(int canvasX, int canvasY)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      */
getAction(String actionId)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      */
onPaint(PaintEvent e)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      */
showInvisibleViews(boolean show)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      */
getNodesToExplode()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      */
clearHover()1085     void clearHover() {
1086         mHoverOverlay.clearHover();
1087     }
1088 
1089     /**
1090      * Hover on top of a known child.
1091      */
hover(MouseEvent e)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      */
show(CanvasViewInfo vi)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      */
showInclude(String url)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      */
getLayoutResourceName()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      */
showXml(CanvasViewInfo vi)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      */
createDragSource(Control control)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      */
createDropTarget(Control control)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      */
setupGlobalActionHandlers()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 
getCutLabel()1381     String getCutLabel() {
1382         return mCutAction.getText();
1383     }
1384 
getDeleteLabel()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      */
updateMenuActionState()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      */
updateGlobalActions(@onNull IActionBars bars)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      */
copyActionAttributes(Action action, ActionFactory factory)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")
createContextMenu()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      */
setupStaticMenuActions(IMenuManager manager)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      */
delete()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      */
createDocumentRoot(final @NonNull SimpleElement root)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      */
getInsets(String fqcn)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 
debugPrintf(String message, Object... params)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 */
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      */
setPreview(@ullable RenderPreview preview)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
getPreview()1705     public RenderPreview getPreview() {
1706         return mPreview;
1707     }
1708 
1709     /** Ensures that the configuration previews are up to date for this canvas */
syncPreviewMode()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