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