| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Eclipse Public License, Version 1.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.eclipse.org/org/documents/epl-v10.php |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.ide.eclipse.adt.internal.editors.layout.gle2; |
| |
| import com.android.SdkConstants; |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.ide.common.api.IDragElement.IDragAttribute; |
| import com.android.ide.common.api.INode; |
| import com.android.ide.common.api.Margins; |
| import com.android.ide.common.api.Point; |
| import com.android.ide.common.rendering.api.Capability; |
| import com.android.ide.common.rendering.api.RenderSession; |
| import com.android.ide.eclipse.adt.AdtPlugin; |
| import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; |
| import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; |
| import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; |
| import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription; |
| import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; |
| import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; |
| import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory; |
| import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; |
| import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository; |
| import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; |
| import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; |
| import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; |
| import com.android.ide.eclipse.adt.internal.lint.LintEditAction; |
| import com.android.resources.Density; |
| |
| import org.eclipse.core.filesystem.EFS; |
| import org.eclipse.core.filesystem.IFileStore; |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IWorkspaceRoot; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.QualifiedName; |
| import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility; |
| import org.eclipse.jface.action.Action; |
| import org.eclipse.jface.action.ActionContributionItem; |
| import org.eclipse.jface.action.IAction; |
| import org.eclipse.jface.action.IContributionItem; |
| import org.eclipse.jface.action.IMenuManager; |
| import org.eclipse.jface.action.IStatusLineManager; |
| import org.eclipse.jface.action.MenuManager; |
| import org.eclipse.jface.action.Separator; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.custom.StyledText; |
| import org.eclipse.swt.dnd.DND; |
| import org.eclipse.swt.dnd.DragSource; |
| import org.eclipse.swt.dnd.DropTarget; |
| import org.eclipse.swt.dnd.TextTransfer; |
| import org.eclipse.swt.dnd.Transfer; |
| import org.eclipse.swt.events.ControlAdapter; |
| import org.eclipse.swt.events.ControlEvent; |
| import org.eclipse.swt.events.KeyEvent; |
| import org.eclipse.swt.events.MenuDetectEvent; |
| import org.eclipse.swt.events.MenuDetectListener; |
| import org.eclipse.swt.events.MouseEvent; |
| import org.eclipse.swt.events.PaintEvent; |
| import org.eclipse.swt.events.PaintListener; |
| import org.eclipse.swt.graphics.Color; |
| import org.eclipse.swt.graphics.Font; |
| import org.eclipse.swt.graphics.GC; |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.swt.graphics.ImageData; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.widgets.Canvas; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Control; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Menu; |
| import org.eclipse.swt.widgets.Shell; |
| import org.eclipse.ui.IActionBars; |
| import org.eclipse.ui.IEditorPart; |
| import org.eclipse.ui.IEditorSite; |
| import org.eclipse.ui.IWorkbenchPage; |
| import org.eclipse.ui.IWorkbenchWindow; |
| import org.eclipse.ui.PartInitException; |
| import org.eclipse.ui.actions.ActionFactory; |
| import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction; |
| import org.eclipse.ui.actions.ContributionItemFactory; |
| import org.eclipse.ui.ide.IDE; |
| import org.eclipse.ui.internal.ide.IDEWorkbenchMessages; |
| import org.eclipse.ui.texteditor.ITextEditor; |
| import org.w3c.dom.Node; |
| |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * Displays the image rendered by the {@link GraphicalEditorPart} and handles |
| * the interaction with the widgets. |
| * <p/> |
| * {@link LayoutCanvas} implements the "Canvas" control. The editor part |
| * actually uses the {@link LayoutCanvasViewer}, which is a JFace viewer wrapper |
| * around this control. |
| * <p/> |
| * The LayoutCanvas contains the painting logic for the canvas. Selection, |
| * clipboard, view management etc. is handled in separate helper classes. |
| * |
| * @since GLE2 |
| */ |
| @SuppressWarnings("restriction") // For WorkBench "Show In" support |
| public class LayoutCanvas extends Canvas { |
| private final static QualifiedName NAME_ZOOM = |
| new QualifiedName(AdtPlugin.PLUGIN_ID, "zoom");//$NON-NLS-1$ |
| |
| private static final boolean DEBUG = false; |
| |
| static final String PREFIX_CANVAS_ACTION = "canvas_action_"; //$NON-NLS-1$ |
| |
| /** The layout editor that uses this layout canvas. */ |
| private final LayoutEditorDelegate mEditorDelegate; |
| |
| /** The Rules Engine, associated with the current project. */ |
| private RulesEngine mRulesEngine; |
| |
| /** GC wrapper given to the IViewRule methods. The GC itself is only defined in the |
| * context of {@link #onPaint(PaintEvent)}; otherwise it is null. */ |
| private GCWrapper mGCWrapper; |
| |
| /** Default font used on the canvas. Do not dispose, it's a system font. */ |
| private Font mFont; |
| |
| /** Current hover view info. Null when no mouse hover. */ |
| private CanvasViewInfo mHoverViewInfo; |
| |
| /** When true, always display the outline of all views. */ |
| private boolean mShowOutline; |
| |
| /** When true, display the outline of all empty parent views. */ |
| private boolean mShowInvisible; |
| |
| /** Drop target associated with this composite. */ |
| private DropTarget mDropTarget; |
| |
| /** Factory that can create {@link INode} proxies. */ |
| private final @NonNull NodeFactory mNodeFactory = new NodeFactory(this); |
| |
| /** Vertical scaling & scrollbar information. */ |
| private final CanvasTransform mVScale; |
| |
| /** Horizontal scaling & scrollbar information. */ |
| private final CanvasTransform mHScale; |
| |
| /** Drag source associated with this canvas. */ |
| private DragSource mDragSource; |
| |
| /** |
| * The current Outline Page, to set its model. |
| * It isn't possible to call OutlinePage2.dispose() in this.dispose(). |
| * this.dispose() is called from GraphicalEditorPart.dispose(), |
| * when page's widget is already disposed. |
| * Added the DisposeListener to OutlinePage2 in order to correctly dispose this page. |
| **/ |
| private OutlinePage mOutlinePage; |
| |
| /** Delete action for the Edit or context menu. */ |
| private Action mDeleteAction; |
| |
| /** Select-All action for the Edit or context menu. */ |
| private Action mSelectAllAction; |
| |
| /** Paste action for the Edit or context menu. */ |
| private Action mPasteAction; |
| |
| /** Cut action for the Edit or context menu. */ |
| private Action mCutAction; |
| |
| /** Copy action for the Edit or context menu. */ |
| private Action mCopyAction; |
| |
| /** Undo action: delegates to the text editor */ |
| private IAction mUndoAction; |
| |
| /** Redo action: delegates to the text editor */ |
| private IAction mRedoAction; |
| |
| /** Root of the context menu. */ |
| private MenuManager mMenuManager; |
| |
| /** The view hierarchy associated with this canvas. */ |
| private final ViewHierarchy mViewHierarchy = new ViewHierarchy(this); |
| |
| /** The selection in the canvas. */ |
| private final SelectionManager mSelectionManager = new SelectionManager(this); |
| |
| /** The overlay which paints the optional outline. */ |
| private OutlineOverlay mOutlineOverlay; |
| |
| /** The overlay which paints outlines around empty children */ |
| private EmptyViewsOverlay mEmptyOverlay; |
| |
| /** The overlay which paints the mouse hover. */ |
| private HoverOverlay mHoverOverlay; |
| |
| /** The overlay which paints the lint warnings */ |
| private LintOverlay mLintOverlay; |
| |
| /** The overlay which paints the selection. */ |
| private SelectionOverlay mSelectionOverlay; |
| |
| /** The overlay which paints the rendered layout image. */ |
| private ImageOverlay mImageOverlay; |
| |
| /** The overlay which paints masks hiding everything but included content. */ |
| private IncludeOverlay mIncludeOverlay; |
| |
| /** Configuration previews shown next to the layout */ |
| private final RenderPreviewManager mPreviewManager; |
| |
| /** |
| * Gesture Manager responsible for identifying mouse, keyboard and drag and |
| * drop events. |
| */ |
| private final GestureManager mGestureManager = new GestureManager(this); |
| |
| /** |
| * When set, performs a zoom-to-fit when the next rendering image arrives. |
| */ |
| private boolean mZoomFitNextImage; |
| |
| /** |
| * Native clipboard support. |
| */ |
| private ClipboardSupport mClipboardSupport; |
| |
| /** Tooltip manager for lint warnings */ |
| private LintTooltipManager mLintTooltipManager; |
| |
| private Color mBackgroundColor; |
| |
| /** |
| * Creates a new {@link LayoutCanvas} widget |
| * |
| * @param editorDelegate the associated editor delegate |
| * @param rulesEngine the rules engine |
| * @param parent parent SWT widget |
| * @param style the SWT style |
| */ |
| public LayoutCanvas(LayoutEditorDelegate editorDelegate, |
| RulesEngine rulesEngine, |
| Composite parent, |
| int style) { |
| super(parent, style | SWT.DOUBLE_BUFFERED | SWT.V_SCROLL | SWT.H_SCROLL); |
| mEditorDelegate = editorDelegate; |
| mRulesEngine = rulesEngine; |
| |
| mBackgroundColor = new Color(parent.getDisplay(), 150, 150, 150); |
| setBackground(mBackgroundColor); |
| |
| mClipboardSupport = new ClipboardSupport(this, parent); |
| mHScale = new CanvasTransform(this, getHorizontalBar()); |
| mVScale = new CanvasTransform(this, getVerticalBar()); |
| mPreviewManager = new RenderPreviewManager(this); |
| |
| // Unit test suite passes a null here; TODO: Replace with mocking |
| IFile file = editorDelegate != null ? editorDelegate.getEditor().getInputFile() : null; |
| if (file != null) { |
| String zoom = AdtPlugin.getFileProperty(file, NAME_ZOOM); |
| if (zoom != null) { |
| try { |
| double initialScale = Double.parseDouble(zoom); |
| if (initialScale > 0.1) { |
| mHScale.setScale(initialScale); |
| mVScale.setScale(initialScale); |
| } |
| } catch (NumberFormatException nfe) { |
| // Ignore - use zoom=100% |
| } |
| } else { |
| mZoomFitNextImage = true; |
| } |
| } |
| |
| mGCWrapper = new GCWrapper(mHScale, mVScale); |
| |
| Display display = getDisplay(); |
| mFont = display.getSystemFont(); |
| |
| // --- Set up graphic overlays |
| // mOutlineOverlay and mEmptyOverlay are initialized lazily |
| mHoverOverlay = new HoverOverlay(this, mHScale, mVScale); |
| mHoverOverlay.create(display); |
| mSelectionOverlay = new SelectionOverlay(this); |
| mSelectionOverlay.create(display); |
| mImageOverlay = new ImageOverlay(this, mHScale, mVScale); |
| mIncludeOverlay = new IncludeOverlay(this); |
| mImageOverlay.create(display); |
| mLintOverlay = new LintOverlay(this); |
| mLintOverlay.create(display); |
| |
| // --- Set up listeners |
| addPaintListener(new PaintListener() { |
| @Override |
| public void paintControl(PaintEvent e) { |
| onPaint(e); |
| } |
| }); |
| |
| addControlListener(new ControlAdapter() { |
| @Override |
| public void controlResized(ControlEvent e) { |
| super.controlResized(e); |
| |
| // Check editor state: |
| LayoutWindowCoordinator coordinator = null; |
| IEditorSite editorSite = getEditorDelegate().getEditor().getEditorSite(); |
| IWorkbenchWindow window = editorSite.getWorkbenchWindow(); |
| if (window != null) { |
| coordinator = LayoutWindowCoordinator.get(window, false); |
| if (coordinator != null) { |
| coordinator.syncMaximizedState(editorSite.getPage()); |
| } |
| } |
| |
| updateScrollBars(); |
| |
| // Update the zoom level in the canvas when you toggle the zoom |
| if (coordinator != null) { |
| mZoomCheck.run(); |
| } else { |
| // During startup, delay updates which can trigger further layout |
| getDisplay().asyncExec(mZoomCheck); |
| |
| } |
| } |
| }); |
| |
| // --- setup drag'n'drop --- |
| // DND Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html |
| |
| mDropTarget = createDropTarget(this); |
| mDragSource = createDragSource(this); |
| mGestureManager.registerListeners(mDragSource, mDropTarget); |
| |
| if (mEditorDelegate == null) { |
| // TODO: In another CL we should use EasyMock/objgen to provide an editor. |
| return; // Unit test |
| } |
| |
| // --- setup context menu --- |
| setupGlobalActionHandlers(); |
| createContextMenu(); |
| |
| // --- setup outline --- |
| // Get the outline associated with this editor, if any and of the right type. |
| if (editorDelegate != null) { |
| mOutlinePage = editorDelegate.getGraphicalOutline(); |
| } |
| |
| mLintTooltipManager = new LintTooltipManager(this); |
| mLintTooltipManager.register(); |
| } |
| |
| void updateScrollBars() { |
| Rectangle clientArea = getClientArea(); |
| Image image = mImageOverlay.getImage(); |
| if (image != null) { |
| ImageData imageData = image.getImageData(); |
| int clientWidth = clientArea.width; |
| int clientHeight = clientArea.height; |
| |
| int imageWidth = imageData.width; |
| int imageHeight = imageData.height; |
| |
| int fullWidth = imageWidth; |
| int fullHeight = imageHeight; |
| |
| if (mPreviewManager.hasPreviews()) { |
| fullHeight = Math.max(fullHeight, |
| (int) (mPreviewManager.getHeight() / mHScale.getScale())); |
| } |
| |
| if (clientWidth == 0) { |
| clientWidth = imageWidth; |
| Shell shell = getShell(); |
| if (shell != null) { |
| org.eclipse.swt.graphics.Point size = shell.getSize(); |
| if (size.x > 0) { |
| clientWidth = size.x * 70 / 100; |
| } |
| } |
| } |
| if (clientHeight == 0) { |
| clientHeight = imageHeight; |
| Shell shell = getShell(); |
| if (shell != null) { |
| org.eclipse.swt.graphics.Point size = shell.getSize(); |
| if (size.y > 0) { |
| clientWidth = size.y * 80 / 100; |
| } |
| } |
| } |
| |
| mHScale.setSize(imageWidth, fullWidth, clientWidth); |
| mVScale.setSize(imageHeight, fullHeight, clientHeight); |
| } |
| } |
| |
| private Runnable mZoomCheck = new Runnable() { |
| private Boolean mWasZoomed; |
| |
| @Override |
| public void run() { |
| if (isDisposed()) { |
| return; |
| } |
| |
| IEditorSite editorSite = getEditorDelegate().getEditor().getEditorSite(); |
| IWorkbenchWindow window = editorSite.getWorkbenchWindow(); |
| if (window != null) { |
| LayoutWindowCoordinator coordinator = LayoutWindowCoordinator.get(window, false); |
| if (coordinator != null) { |
| Boolean zoomed = coordinator.isEditorMaximized(); |
| if (mWasZoomed != zoomed) { |
| if (mWasZoomed != null) { |
| LayoutActionBar actionBar = getGraphicalEditor().getLayoutActionBar(); |
| if (actionBar.isZoomingAllowed()) { |
| setFitScale(true /*onlyZoomOut*/, true /*allowZoomIn*/); |
| } |
| } |
| mWasZoomed = zoomed; |
| } |
| } |
| } |
| } |
| }; |
| |
| void handleKeyPressed(KeyEvent e) { |
| // Set up backspace as an alias for the delete action within the canvas. |
| // On most Macs there is no delete key - though there IS a key labeled |
| // "Delete" and it sends a backspace key code! In short, for Macs we should |
| // treat backspace as delete, and it's harmless (and probably useful) to |
| // handle backspace for other platforms as well. |
| if (e.keyCode == SWT.BS) { |
| mDeleteAction.run(); |
| } else if (e.keyCode == SWT.ESC) { |
| mSelectionManager.selectParent(); |
| } else if (e.keyCode == DynamicContextMenu.DEFAULT_ACTION_KEY) { |
| mSelectionManager.performDefaultAction(); |
| } else if (e.keyCode == 'r') { |
| // Keep key bindings in sync with {@link DynamicContextMenu#createPlainAction} |
| // TODO: Find a way to look up the Eclipse key bindings and attempt |
| // to use the current keymap's rename action. |
| if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) { |
| // Command+Option+R |
| if ((e.stateMask & (SWT.MOD1 | SWT.MOD3)) == (SWT.MOD1 | SWT.MOD3)) { |
| mSelectionManager.performRename(); |
| } |
| } else { |
| // Alt+Shift+R |
| if ((e.stateMask & (SWT.MOD2 | SWT.MOD3)) == (SWT.MOD2 | SWT.MOD3)) { |
| mSelectionManager.performRename(); |
| } |
| } |
| } else { |
| // Zooming actions |
| char c = e.character; |
| LayoutActionBar actionBar = getGraphicalEditor().getLayoutActionBar(); |
| if (c == '1' && actionBar.isZoomingAllowed()) { |
| setScale(1, true); |
| } else if (c == '0' && actionBar.isZoomingAllowed()) { |
| setFitScale(true, true /*allowZoomIn*/); |
| } else if (e.keyCode == '0' && (e.stateMask & SWT.MOD2) != 0 |
| && actionBar.isZoomingAllowed()) { |
| setFitScale(false, true /*allowZoomIn*/); |
| } else if ((c == '+' || c == '=') && actionBar.isZoomingAllowed()) { |
| if ((e.stateMask & SWT.MOD1) != 0) { |
| mPreviewManager.zoomIn(); |
| } else { |
| actionBar.rescale(1); |
| } |
| } else if (c == '-' && actionBar.isZoomingAllowed()) { |
| if ((e.stateMask & SWT.MOD1) != 0) { |
| mPreviewManager.zoomOut(); |
| } else { |
| actionBar.rescale(-1); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void dispose() { |
| super.dispose(); |
| |
| mGestureManager.unregisterListeners(mDragSource, mDropTarget); |
| |
| if (mLintTooltipManager != null) { |
| mLintTooltipManager.unregister(); |
| mLintTooltipManager = null; |
| } |
| |
| if (mDropTarget != null) { |
| mDropTarget.dispose(); |
| mDropTarget = null; |
| } |
| |
| if (mRulesEngine != null) { |
| mRulesEngine.dispose(); |
| mRulesEngine = null; |
| } |
| |
| if (mDragSource != null) { |
| mDragSource.dispose(); |
| mDragSource = null; |
| } |
| |
| if (mClipboardSupport != null) { |
| mClipboardSupport.dispose(); |
| mClipboardSupport = null; |
| } |
| |
| if (mGCWrapper != null) { |
| mGCWrapper.dispose(); |
| mGCWrapper = null; |
| } |
| |
| if (mOutlineOverlay != null) { |
| mOutlineOverlay.dispose(); |
| mOutlineOverlay = null; |
| } |
| |
| if (mEmptyOverlay != null) { |
| mEmptyOverlay.dispose(); |
| mEmptyOverlay = null; |
| } |
| |
| if (mHoverOverlay != null) { |
| mHoverOverlay.dispose(); |
| mHoverOverlay = null; |
| } |
| |
| if (mSelectionOverlay != null) { |
| mSelectionOverlay.dispose(); |
| mSelectionOverlay = null; |
| } |
| |
| if (mImageOverlay != null) { |
| mImageOverlay.dispose(); |
| mImageOverlay = null; |
| } |
| |
| if (mIncludeOverlay != null) { |
| mIncludeOverlay.dispose(); |
| mIncludeOverlay = null; |
| } |
| |
| if (mLintOverlay != null) { |
| mLintOverlay.dispose(); |
| mLintOverlay = null; |
| } |
| |
| if (mBackgroundColor != null) { |
| mBackgroundColor.dispose(); |
| mBackgroundColor = null; |
| } |
| |
| mPreviewManager.disposePreviews(); |
| mViewHierarchy.dispose(); |
| } |
| |
| /** |
| * Returns the configuration preview manager for this canvas |
| * |
| * @return the configuration preview manager for this canvas |
| */ |
| @NonNull |
| public RenderPreviewManager getPreviewManager() { |
| return mPreviewManager; |
| } |
| |
| /** Returns the Rules Engine, associated with the current project. */ |
| RulesEngine getRulesEngine() { |
| return mRulesEngine; |
| } |
| |
| /** Sets the Rules Engine, associated with the current project. */ |
| void setRulesEngine(RulesEngine rulesEngine) { |
| mRulesEngine = rulesEngine; |
| } |
| |
| /** |
| * Returns the factory to use to convert from {@link CanvasViewInfo} or from |
| * {@link UiViewElementNode} to {@link INode} proxies. |
| * |
| * @return the node factory |
| */ |
| @NonNull |
| public NodeFactory getNodeFactory() { |
| return mNodeFactory; |
| } |
| |
| /** |
| * Returns the GCWrapper used to paint view rules. |
| * |
| * @return The GCWrapper used to paint view rules |
| */ |
| GCWrapper getGcWrapper() { |
| return mGCWrapper; |
| } |
| |
| /** |
| * Returns the {@link LayoutEditorDelegate} associated with this canvas. |
| * |
| * @return the delegate |
| */ |
| public LayoutEditorDelegate getEditorDelegate() { |
| return mEditorDelegate; |
| } |
| |
| /** |
| * Returns the current {@link ImageOverlay} painting the rendered result |
| * |
| * @return the image overlay responsible for painting the rendered result, never null |
| */ |
| ImageOverlay getImageOverlay() { |
| return mImageOverlay; |
| } |
| |
| /** |
| * Returns the current {@link SelectionOverlay} painting the selection highlights |
| * |
| * @return the selection overlay responsible for painting the selection highlights, |
| * never null |
| */ |
| SelectionOverlay getSelectionOverlay() { |
| return mSelectionOverlay; |
| } |
| |
| /** |
| * Returns the {@link GestureManager} associated with this canvas. |
| * |
| * @return the {@link GestureManager} associated with this canvas, never null. |
| */ |
| GestureManager getGestureManager() { |
| return mGestureManager; |
| } |
| |
| /** |
| * Returns the current {@link HoverOverlay} painting the mouse hover. |
| * |
| * @return the hover overlay responsible for painting the mouse hover, |
| * never null |
| */ |
| HoverOverlay getHoverOverlay() { |
| return mHoverOverlay; |
| } |
| |
| /** |
| * Returns the horizontal {@link CanvasTransform} transform object, which can map |
| * a layout point into a control point. |
| * |
| * @return A {@link CanvasTransform} for mapping between layout and control |
| * coordinates in the horizontal dimension. |
| */ |
| CanvasTransform getHorizontalTransform() { |
| return mHScale; |
| } |
| |
| /** |
| * Returns the vertical {@link CanvasTransform} transform object, which can map a |
| * layout point into a control point. |
| * |
| * @return A {@link CanvasTransform} for mapping between layout and control |
| * coordinates in the vertical dimension. |
| */ |
| CanvasTransform getVerticalTransform() { |
| return mVScale; |
| } |
| |
| /** |
| * Returns the {@link OutlinePage} associated with this canvas |
| * |
| * @return the {@link OutlinePage} associated with this canvas |
| */ |
| public OutlinePage getOutlinePage() { |
| return mOutlinePage; |
| } |
| |
| /** |
| * Returns the {@link SelectionManager} associated with this canvas. |
| * |
| * @return The {@link SelectionManager} holding the selection for this |
| * canvas. Never null. |
| */ |
| public SelectionManager getSelectionManager() { |
| return mSelectionManager; |
| } |
| |
| /** |
| * Returns the {@link ViewHierarchy} object associated with this canvas, |
| * holding the most recent rendered view of the scene, if valid. |
| * |
| * @return The {@link ViewHierarchy} object associated with this canvas. |
| * Never null. |
| */ |
| public ViewHierarchy getViewHierarchy() { |
| return mViewHierarchy; |
| } |
| |
| /** |
| * Returns the {@link ClipboardSupport} object associated with this canvas. |
| * |
| * @return The {@link ClipboardSupport} object for this canvas. Null only after dispose. |
| */ |
| public ClipboardSupport getClipboardSupport() { |
| return mClipboardSupport; |
| } |
| |
| /** Returns the Select All action bound to this canvas */ |
| Action getSelectAllAction() { |
| return mSelectAllAction; |
| } |
| |
| /** Returns the associated {@link GraphicalEditorPart} */ |
| GraphicalEditorPart getGraphicalEditor() { |
| return mEditorDelegate.getGraphicalEditor(); |
| } |
| |
| /** |
| * Sets the result of the layout rendering. The result object indicates if the layout |
| * rendering succeeded. If it did, it contains a bitmap and the objects rectangles. |
| * |
| * Implementation detail: the bridge's computeLayout() method already returns a newly |
| * allocated ILayourResult. That means we can keep this result and hold on to it |
| * when it is valid. |
| * |
| * @param session The new scene, either valid or not. |
| * @param explodedNodes The set of individual nodes the layout computer was asked to |
| * explode. Note that these are independent of the explode-all mode where |
| * all views are exploded; this is used only for the mode ( |
| * {@link #showInvisibleViews(boolean)}) where individual invisible nodes |
| * are padded during certain interactions. |
| */ |
| void setSession(RenderSession session, Set<UiElementNode> explodedNodes, |
| boolean layoutlib5) { |
| // disable any hover |
| clearHover(); |
| |
| mViewHierarchy.setSession(session, explodedNodes, layoutlib5); |
| if (mViewHierarchy.isValid() && session != null) { |
| Image image = mImageOverlay.setImage(session.getImage(), |
| session.isAlphaChannelImage()); |
| |
| mOutlinePage.setModel(mViewHierarchy.getRoot()); |
| getGraphicalEditor().setModel(mViewHierarchy.getRoot()); |
| |
| if (image != null) { |
| updateScrollBars(); |
| if (mZoomFitNextImage) { |
| // Must be run asynchronously because getClientArea() returns 0 bounds |
| // when the editor is being initialized |
| getDisplay().asyncExec(new Runnable() { |
| @Override |
| public void run() { |
| if (!isDisposed()) { |
| ensureZoomed(); |
| } |
| } |
| }); |
| } |
| |
| // Ensure that if we have a a preview mode enabled, it's shown |
| syncPreviewMode(); |
| } |
| } |
| |
| redraw(); |
| } |
| |
| void ensureZoomed() { |
| if (mZoomFitNextImage && getClientArea().height > 0) { |
| mZoomFitNextImage = false; |
| LayoutActionBar actionBar = getGraphicalEditor().getLayoutActionBar(); |
| if (actionBar.isZoomingAllowed()) { |
| setFitScale(true, true /*allowZoomIn*/); |
| } |
| } |
| } |
| |
| void setShowOutline(boolean newState) { |
| mShowOutline = newState; |
| redraw(); |
| } |
| |
| /** |
| * Returns the zoom scale factor of the canvas (the amount the full |
| * resolution render of the device is zoomed before being shown on the |
| * canvas) |
| * |
| * @return the image scale |
| */ |
| public double getScale() { |
| return mHScale.getScale(); |
| } |
| |
| void setScale(double scale, boolean redraw) { |
| if (scale <= 0.0) { |
| scale = 1.0; |
| } |
| |
| if (scale == getScale()) { |
| return; |
| } |
| |
| mHScale.setScale(scale); |
| mVScale.setScale(scale); |
| if (redraw) { |
| redraw(); |
| } |
| |
| // Clear the zoom setting if it is almost identical to 1.0 |
| String zoomValue = (Math.abs(scale - 1.0) < 0.0001) ? null : Double.toString(scale); |
| IFile file = mEditorDelegate.getEditor().getInputFile(); |
| if (file != null) { |
| AdtPlugin.setFileProperty(file, NAME_ZOOM, zoomValue); |
| } |
| } |
| |
| /** |
| * Scales the canvas to best fit |
| * |
| * @param onlyZoomOut if true, then the zooming factor will never be larger than 1, |
| * which means that this function will zoom out if necessary to show the |
| * rendered image, but it will never zoom in. |
| * TODO: Rename this, it sounds like it conflicts with allowZoomIn, |
| * even though one is referring to the zoom level and one is referring |
| * to the overall act of scaling above/below 1. |
| * @param allowZoomIn if false, then if the computed zoom factor is smaller than |
| * the current zoom factor, it will be ignored. |
| */ |
| public void setFitScale(boolean onlyZoomOut, boolean allowZoomIn) { |
| ImageOverlay imageOverlay = getImageOverlay(); |
| if (imageOverlay == null) { |
| return; |
| } |
| Image image = imageOverlay.getImage(); |
| if (image != null) { |
| Rectangle canvasSize = getClientArea(); |
| int canvasWidth = canvasSize.width; |
| int canvasHeight = canvasSize.height; |
| |
| boolean hasPreviews = mPreviewManager.hasPreviews(); |
| if (hasPreviews) { |
| canvasWidth = 2 * canvasWidth / 3; |
| } else { |
| canvasWidth -= 4; |
| canvasHeight -= 4; |
| } |
| |
| ImageData imageData = image.getImageData(); |
| int sceneWidth = imageData.width; |
| int sceneHeight = imageData.height; |
| if (sceneWidth == 0.0 || sceneHeight == 0.0) { |
| return; |
| } |
| |
| if (imageOverlay.getShowDropShadow()) { |
| sceneWidth += 2 * ImageUtils.SHADOW_SIZE; |
| sceneHeight += 2 * ImageUtils.SHADOW_SIZE; |
| } |
| |
| // Reduce the margins if necessary |
| int hDelta = canvasWidth - sceneWidth; |
| int hMargin = 0; |
| if (hDelta > 2 * CanvasTransform.DEFAULT_MARGIN) { |
| hMargin = CanvasTransform.DEFAULT_MARGIN; |
| } else if (hDelta > 0) { |
| hMargin = hDelta / 2; |
| } |
| |
| int vDelta = canvasHeight - sceneHeight; |
| int vMargin = 0; |
| if (vDelta > 2 * CanvasTransform.DEFAULT_MARGIN) { |
| vMargin = CanvasTransform.DEFAULT_MARGIN; |
| } else if (vDelta > 0) { |
| vMargin = vDelta / 2; |
| } |
| |
| double hScale = (canvasWidth - 2 * hMargin) / (double) sceneWidth; |
| double vScale = (canvasHeight - 2 * vMargin) / (double) sceneHeight; |
| |
| double scale = Math.min(hScale, vScale); |
| |
| if (onlyZoomOut) { |
| scale = Math.min(1.0, scale); |
| } |
| |
| if (!allowZoomIn && scale > getScale()) { |
| return; |
| } |
| |
| setScale(scale, true); |
| } |
| } |
| |
| /** |
| * Transforms a point, expressed in layout coordinates, into "client" coordinates |
| * relative to the control (and not relative to the display). |
| * |
| * @param canvasX X in the canvas coordinates |
| * @param canvasY Y in the canvas coordinates |
| * @return A new {@link Point} in control client coordinates (not display coordinates) |
| */ |
| Point layoutToControlPoint(int canvasX, int canvasY) { |
| int x = mHScale.translate(canvasX); |
| int y = mVScale.translate(canvasY); |
| return new Point(x, y); |
| } |
| |
| /** |
| * Returns the action for the context menu corresponding to the given action id. |
| * <p/> |
| * For global actions such as copy or paste, the action id must be composed of |
| * the {@link #PREFIX_CANVAS_ACTION} followed by one of {@link ActionFactory}'s |
| * action ids. |
| * <p/> |
| * Returns null if there's no action for the given id. |
| */ |
| IAction getAction(String actionId) { |
| String prefix = PREFIX_CANVAS_ACTION; |
| if (mMenuManager == null || |
| actionId == null || |
| !actionId.startsWith(prefix)) { |
| return null; |
| } |
| |
| actionId = actionId.substring(prefix.length()); |
| |
| for (IContributionItem contrib : mMenuManager.getItems()) { |
| if (contrib instanceof ActionContributionItem && |
| actionId.equals(contrib.getId())) { |
| return ((ActionContributionItem) contrib).getAction(); |
| } |
| } |
| |
| return null; |
| } |
| |
| //--------------- |
| |
| /** |
| * Paints the canvas in response to paint events. |
| */ |
| private void onPaint(PaintEvent e) { |
| GC gc = e.gc; |
| gc.setFont(mFont); |
| mGCWrapper.setGC(gc); |
| try { |
| if (!mImageOverlay.isHiding()) { |
| mImageOverlay.paint(gc); |
| } |
| |
| mPreviewManager.paint(gc); |
| |
| if (mShowOutline) { |
| if (mOutlineOverlay == null) { |
| mOutlineOverlay = new OutlineOverlay(mViewHierarchy, mHScale, mVScale); |
| mOutlineOverlay.create(getDisplay()); |
| } |
| if (!mOutlineOverlay.isHiding()) { |
| mOutlineOverlay.paint(gc); |
| } |
| } |
| |
| if (mShowInvisible) { |
| if (mEmptyOverlay == null) { |
| mEmptyOverlay = new EmptyViewsOverlay(mViewHierarchy, mHScale, mVScale); |
| mEmptyOverlay.create(getDisplay()); |
| } |
| if (!mEmptyOverlay.isHiding()) { |
| mEmptyOverlay.paint(gc); |
| } |
| } |
| |
| if (!mHoverOverlay.isHiding()) { |
| mHoverOverlay.paint(gc); |
| } |
| |
| if (!mLintOverlay.isHiding()) { |
| mLintOverlay.paint(gc); |
| } |
| |
| if (!mIncludeOverlay.isHiding()) { |
| mIncludeOverlay.paint(gc); |
| } |
| |
| if (!mSelectionOverlay.isHiding()) { |
| mSelectionOverlay.paint(mSelectionManager, mGCWrapper, gc, mRulesEngine); |
| } |
| mGestureManager.paint(gc); |
| |
| } finally { |
| mGCWrapper.setGC(null); |
| } |
| } |
| |
| /** |
| * Shows or hides invisible parent views, which are views which have empty bounds and |
| * no children. The nodes which will be shown are provided by |
| * {@link #getNodesToExplode()}. |
| * |
| * @param show When true, any invisible parent nodes are padded and highlighted |
| * ("exploded"), and when false any formerly exploded nodes are hidden. |
| */ |
| void showInvisibleViews(boolean show) { |
| if (mShowInvisible == show) { |
| return; |
| } |
| mShowInvisible = show; |
| |
| // Optimization: Avoid doing work when we don't have invisible parents (on show) |
| // or formerly exploded nodes (on hide). |
| if (show && !mViewHierarchy.hasInvisibleParents()) { |
| return; |
| } else if (!show && !mViewHierarchy.hasExplodedParents()) { |
| return; |
| } |
| |
| mEditorDelegate.recomputeLayout(); |
| } |
| |
| /** |
| * Returns a set of nodes that should be exploded (forced non-zero padding during render), |
| * or null if no nodes should be exploded. (Note that this is independent of the |
| * explode-all mode, where all nodes are padded -- that facility does not use this |
| * mechanism, which is only intended to be used to expose invisible parent nodes. |
| * |
| * @return The set of invisible parents, or null if no views should be expanded. |
| */ |
| public Set<UiElementNode> getNodesToExplode() { |
| if (mShowInvisible) { |
| return mViewHierarchy.getInvisibleNodes(); |
| } |
| |
| // IF we have selection, and IF we have invisible nodes in the view, |
| // see if any of the selected items are among the invisible nodes, and if so |
| // add them to a lazily constructed set which we pass back for rendering. |
| Set<UiElementNode> result = null; |
| List<SelectionItem> selections = mSelectionManager.getSelections(); |
| if (selections.size() > 0) { |
| List<CanvasViewInfo> invisibleParents = mViewHierarchy.getInvisibleViews(); |
| if (invisibleParents.size() > 0) { |
| for (SelectionItem item : selections) { |
| CanvasViewInfo viewInfo = item.getViewInfo(); |
| // O(n^2) here, but both the selection size and especially the |
| // invisibleParents size are expected to be small |
| if (invisibleParents.contains(viewInfo)) { |
| UiViewElementNode node = viewInfo.getUiViewNode(); |
| if (node != null) { |
| if (result == null) { |
| result = new HashSet<UiElementNode>(); |
| } |
| result.add(node); |
| } |
| } |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Clears the hover. |
| */ |
| void clearHover() { |
| mHoverOverlay.clearHover(); |
| } |
| |
| /** |
| * Hover on top of a known child. |
| */ |
| void hover(MouseEvent e) { |
| // Check if a button is pressed; no hovers during drags |
| if ((e.stateMask & SWT.BUTTON_MASK) != 0) { |
| clearHover(); |
| return; |
| } |
| |
| LayoutPoint p = ControlPoint.create(this, e).toLayout(); |
| CanvasViewInfo vi = mViewHierarchy.findViewInfoAt(p); |
| |
| // We don't hover on the root since it's not a widget per see and it is always there. |
| // We also skip spacers... |
| if (vi != null && (vi.isRoot() || vi.isHidden())) { |
| vi = null; |
| } |
| |
| boolean needsUpdate = vi != mHoverViewInfo; |
| mHoverViewInfo = vi; |
| |
| if (vi == null) { |
| clearHover(); |
| } else { |
| Rectangle r = vi.getSelectionRect(); |
| mHoverOverlay.setHover(r.x, r.y, r.width, r.height); |
| } |
| |
| if (needsUpdate) { |
| redraw(); |
| } |
| } |
| |
| /** |
| * Shows the given {@link CanvasViewInfo}, which can mean exposing its XML or if it's |
| * an included element, its corresponding file. |
| * |
| * @param vi the {@link CanvasViewInfo} to be shown |
| */ |
| public void show(CanvasViewInfo vi) { |
| String url = vi.getIncludeUrl(); |
| if (url != null) { |
| showInclude(url); |
| } else { |
| showXml(vi); |
| } |
| } |
| |
| /** |
| * Shows the layout file referenced by the given url in the same project. |
| * |
| * @param url The layout attribute url of the form @layout/foo |
| */ |
| private void showInclude(String url) { |
| GraphicalEditorPart graphicalEditor = getGraphicalEditor(); |
| IPath filePath = graphicalEditor.findResourceFile(url); |
| if (filePath == null) { |
| // Should not be possible - if the URL had been bad, then we wouldn't |
| // have been able to render the scene and you wouldn't have been able |
| // to click on it |
| return; |
| } |
| |
| // Save the including file, if necessary: without it, the "Show Included In" |
| // facility which is invoked automatically will not work properly if the <include> |
| // tag is not in the saved version of the file, since the outer file is read from |
| // disk rather than from memory. |
| IEditorSite editorSite = graphicalEditor.getEditorSite(); |
| IWorkbenchPage page = editorSite.getPage(); |
| page.saveEditor(mEditorDelegate.getEditor(), false); |
| |
| IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot(); |
| IFile xmlFile = null; |
| IPath workspacePath = workspace.getLocation(); |
| if (workspacePath.isPrefixOf(filePath)) { |
| IPath relativePath = filePath.makeRelativeTo(workspacePath); |
| xmlFile = (IFile) workspace.findMember(relativePath); |
| } else if (filePath.isAbsolute()) { |
| xmlFile = workspace.getFileForLocation(filePath); |
| } |
| if (xmlFile != null) { |
| IFile leavingFile = graphicalEditor.getEditedFile(); |
| Reference next = Reference.create(graphicalEditor.getEditedFile()); |
| |
| try { |
| IEditorPart openAlready = EditorUtility.isOpenInEditor(xmlFile); |
| |
| // Show the included file as included within this click source? |
| if (openAlready != null) { |
| LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(openAlready); |
| if (delegate != null) { |
| GraphicalEditorPart gEditor = delegate.getGraphicalEditor(); |
| if (gEditor != null && |
| gEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) { |
| gEditor.showIn(next); |
| } |
| } |
| } else { |
| try { |
| // Set initial state of a new file |
| // TODO: Only set rendering target portion of the state |
| String state = ConfigurationDescription.getDescription(leavingFile); |
| xmlFile.setSessionProperty(GraphicalEditorPart.NAME_INITIAL_STATE, |
| state); |
| } catch (CoreException e) { |
| // pass |
| } |
| |
| if (graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) { |
| try { |
| xmlFile.setSessionProperty(GraphicalEditorPart.NAME_INCLUDE, next); |
| } catch (CoreException e) { |
| // pass - worst that can happen is that we don't |
| //start with inclusion |
| } |
| } |
| } |
| |
| EditorUtility.openInEditor(xmlFile, true); |
| return; |
| } catch (PartInitException ex) { |
| AdtPlugin.log(ex, "Can't open %$1s", url); //$NON-NLS-1$ |
| } |
| } else { |
| // It's not a path in the workspace; look externally |
| // (this is probably an @android: path) |
| if (filePath.isAbsolute()) { |
| IFileStore fileStore = EFS.getLocalFileSystem().getStore(filePath); |
| // fileStore = fileStore.getChild(names[i]); |
| if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) { |
| try { |
| IDE.openEditorOnFileStore(page, fileStore); |
| return; |
| } catch (PartInitException ex) { |
| AdtPlugin.log(ex, "Can't open %$1s", url); //$NON-NLS-1$ |
| } |
| } |
| } |
| } |
| |
| // Failed: display message to the user |
| String message = String.format("Could not find resource %1$s", url); |
| IStatusLineManager status = editorSite.getActionBars().getStatusLineManager(); |
| status.setErrorMessage(message); |
| getDisplay().beep(); |
| } |
| |
| /** |
| * Returns the layout resource name of this layout |
| * |
| * @return the layout resource name of this layout |
| */ |
| public String getLayoutResourceName() { |
| GraphicalEditorPart graphicalEditor = getGraphicalEditor(); |
| return graphicalEditor.getLayoutResourceName(); |
| } |
| |
| /** |
| * Returns the layout resource url of the current layout |
| * |
| * @return |
| */ |
| /* |
| public String getMe() { |
| GraphicalEditorPart graphicalEditor = getGraphicalEditor(); |
| IFile editedFile = graphicalEditor.getEditedFile(); |
| return editedFile.getProjectRelativePath().toOSString(); |
| } |
| */ |
| |
| /** |
| * Show the XML element corresponding to the given {@link CanvasViewInfo} (unless it's |
| * a root). |
| * |
| * @param vi The clicked {@link CanvasViewInfo} whose underlying XML element we want |
| * to view |
| */ |
| private void showXml(CanvasViewInfo vi) { |
| // Warp to the text editor and show the corresponding XML for the |
| // double-clicked widget |
| if (vi.isRoot()) { |
| return; |
| } |
| |
| Node xmlNode = vi.getXmlNode(); |
| if (xmlNode != null) { |
| boolean found = mEditorDelegate.getEditor().show(xmlNode); |
| if (!found) { |
| getDisplay().beep(); |
| } |
| } |
| } |
| |
| //--------------- |
| |
| /** |
| * Helper to create the drag source for the given control. |
| * <p/> |
| * This is static with package-access so that {@link OutlinePage} can also |
| * create an exact copy of the source with the same attributes. |
| */ |
| /* package */static DragSource createDragSource(Control control) { |
| DragSource source = new DragSource(control, DND.DROP_COPY | DND.DROP_MOVE); |
| source.setTransfer(new Transfer[] { |
| TextTransfer.getInstance(), |
| SimpleXmlTransfer.getInstance() |
| }); |
| return source; |
| } |
| |
| /** |
| * Helper to create the drop target for the given control. |
| */ |
| private static DropTarget createDropTarget(Control control) { |
| DropTarget dropTarget = new DropTarget( |
| control, DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT); |
| dropTarget.setTransfer(new Transfer[] { |
| SimpleXmlTransfer.getInstance() |
| }); |
| return dropTarget; |
| } |
| |
| //--------------- |
| |
| /** |
| * Invoked by the constructor to add our cut/copy/paste/delete/select-all |
| * handlers in the global action handlers of this editor's site. |
| * <p/> |
| * This will enable the menu items under the global Edit menu and make them |
| * invoke our actions as needed. As a benefit, the corresponding shortcut |
| * accelerators will do what one would expect. |
| */ |
| private void setupGlobalActionHandlers() { |
| mCutAction = new Action() { |
| @Override |
| public void run() { |
| mClipboardSupport.cutSelectionToClipboard(mSelectionManager.getSnapshot()); |
| updateMenuActionState(); |
| } |
| }; |
| |
| copyActionAttributes(mCutAction, ActionFactory.CUT); |
| |
| mCopyAction = new Action() { |
| @Override |
| public void run() { |
| mClipboardSupport.copySelectionToClipboard(mSelectionManager.getSnapshot()); |
| updateMenuActionState(); |
| } |
| }; |
| |
| copyActionAttributes(mCopyAction, ActionFactory.COPY); |
| |
| mPasteAction = new Action() { |
| @Override |
| public void run() { |
| mClipboardSupport.pasteSelection(mSelectionManager.getSnapshot()); |
| updateMenuActionState(); |
| } |
| }; |
| |
| copyActionAttributes(mPasteAction, ActionFactory.PASTE); |
| |
| mDeleteAction = new Action() { |
| @Override |
| public void run() { |
| mClipboardSupport.deleteSelection( |
| getDeleteLabel(), |
| mSelectionManager.getSnapshot()); |
| } |
| }; |
| |
| copyActionAttributes(mDeleteAction, ActionFactory.DELETE); |
| |
| mSelectAllAction = new Action() { |
| @Override |
| public void run() { |
| GraphicalEditorPart graphicalEditor = getEditorDelegate().getGraphicalEditor(); |
| StyledText errorLabel = graphicalEditor.getErrorLabel(); |
| if (errorLabel.isFocusControl()) { |
| errorLabel.selectAll(); |
| return; |
| } |
| |
| mSelectionManager.selectAll(); |
| } |
| }; |
| |
| copyActionAttributes(mSelectAllAction, ActionFactory.SELECT_ALL); |
| } |
| |
| String getCutLabel() { |
| return mCutAction.getText(); |
| } |
| |
| String getDeleteLabel() { |
| // verb "Delete" from the DELETE action's title |
| return mDeleteAction.getText(); |
| } |
| |
| /** |
| * Updates menu actions that depends on the selection. |
| */ |
| void updateMenuActionState() { |
| List<SelectionItem> selections = getSelectionManager().getSelections(); |
| boolean hasSelection = !selections.isEmpty(); |
| if (hasSelection && selections.size() == 1 && selections.get(0).isRoot()) { |
| hasSelection = false; |
| } |
| |
| StyledText errorLabel = getGraphicalEditor().getErrorLabel(); |
| mCutAction.setEnabled(hasSelection); |
| mCopyAction.setEnabled(hasSelection || errorLabel.getSelectionCount() > 0); |
| mDeleteAction.setEnabled(hasSelection); |
| // Select All should *always* be selectable, regardless of whether anything |
| // is currently selected. |
| mSelectAllAction.setEnabled(true); |
| |
| // The paste operation is only available if we can paste our custom type. |
| // We do not currently support pasting random text (e.g. XML). Maybe later. |
| boolean hasSxt = mClipboardSupport.hasSxtOnClipboard(); |
| mPasteAction.setEnabled(hasSxt); |
| } |
| |
| /** |
| * Update the actions when this editor is activated |
| * |
| * @param bars the action bar for this canvas |
| */ |
| public void updateGlobalActions(@NonNull IActionBars bars) { |
| updateMenuActionState(); |
| |
| ITextEditor editor = mEditorDelegate.getEditor().getStructuredTextEditor(); |
| boolean graphical = getEditorDelegate().getEditor().getActivePage() == 0; |
| if (graphical) { |
| bars.setGlobalActionHandler(ActionFactory.CUT.getId(), mCutAction); |
| bars.setGlobalActionHandler(ActionFactory.COPY.getId(), mCopyAction); |
| bars.setGlobalActionHandler(ActionFactory.PASTE.getId(), mPasteAction); |
| bars.setGlobalActionHandler(ActionFactory.DELETE.getId(), mDeleteAction); |
| bars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), mSelectAllAction); |
| |
| // Delegate the Undo and Redo actions to the text editor ones, but wrap them |
| // such that we run lint to update the results on the current page (this is |
| // normally done on each editor operation that goes through |
| // {@link AndroidXmlEditor#wrapUndoEditXmlModel}, but not undo/redo) |
| if (mUndoAction == null) { |
| IAction undoAction = editor.getAction(ActionFactory.UNDO.getId()); |
| mUndoAction = new LintEditAction(undoAction, getEditorDelegate().getEditor()); |
| } |
| bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), mUndoAction); |
| if (mRedoAction == null) { |
| IAction redoAction = editor.getAction(ActionFactory.REDO.getId()); |
| mRedoAction = new LintEditAction(redoAction, getEditorDelegate().getEditor()); |
| } |
| bars.setGlobalActionHandler(ActionFactory.REDO.getId(), mRedoAction); |
| } else { |
| bars.setGlobalActionHandler(ActionFactory.CUT.getId(), |
| editor.getAction(ActionFactory.CUT.getId())); |
| bars.setGlobalActionHandler(ActionFactory.COPY.getId(), |
| editor.getAction(ActionFactory.COPY.getId())); |
| bars.setGlobalActionHandler(ActionFactory.PASTE.getId(), |
| editor.getAction(ActionFactory.PASTE.getId())); |
| bars.setGlobalActionHandler(ActionFactory.DELETE.getId(), |
| editor.getAction(ActionFactory.DELETE.getId())); |
| bars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), |
| editor.getAction(ActionFactory.SELECT_ALL.getId())); |
| bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), |
| editor.getAction(ActionFactory.UNDO.getId())); |
| bars.setGlobalActionHandler(ActionFactory.REDO.getId(), |
| editor.getAction(ActionFactory.REDO.getId())); |
| } |
| |
| bars.updateActionBars(); |
| } |
| |
| /** |
| * Helper for {@link #setupGlobalActionHandlers()}. |
| * Copies the action attributes form the given {@link ActionFactory}'s action to |
| * our action. |
| * <p/> |
| * {@link ActionFactory} provides access to the standard global actions in Eclipse. |
| * <p/> |
| * This allows us to grab the standard labels and icons for the |
| * global actions such as copy, cut, paste, delete and select-all. |
| */ |
| private void copyActionAttributes(Action action, ActionFactory factory) { |
| IWorkbenchAction wa = factory.create( |
| mEditorDelegate.getEditor().getEditorSite().getWorkbenchWindow()); |
| action.setId(wa.getId()); |
| action.setText(wa.getText()); |
| action.setEnabled(wa.isEnabled()); |
| action.setDescription(wa.getDescription()); |
| action.setToolTipText(wa.getToolTipText()); |
| action.setAccelerator(wa.getAccelerator()); |
| action.setActionDefinitionId(wa.getActionDefinitionId()); |
| action.setImageDescriptor(wa.getImageDescriptor()); |
| action.setHoverImageDescriptor(wa.getHoverImageDescriptor()); |
| action.setDisabledImageDescriptor(wa.getDisabledImageDescriptor()); |
| action.setHelpListener(wa.getHelpListener()); |
| } |
| |
| /** |
| * Creates the context menu for the canvas. This is called once from the canvas' constructor. |
| * <p/> |
| * The menu has a static part with actions that are always available such as |
| * copy, cut, paste and show in > explorer. This is created by |
| * {@link #setupStaticMenuActions(IMenuManager)}. |
| * <p/> |
| * There's also a dynamic part that is populated by the rules of the |
| * selected elements, created by {@link DynamicContextMenu}. |
| */ |
| @SuppressWarnings("unused") |
| private void createContextMenu() { |
| |
| // This manager is the root of the context menu. |
| mMenuManager = new MenuManager() { |
| @Override |
| public boolean isDynamic() { |
| return true; |
| } |
| }; |
| |
| // Fill the menu manager with the static & dynamic actions |
| setupStaticMenuActions(mMenuManager); |
| new DynamicContextMenu(mEditorDelegate, this, mMenuManager); |
| Menu menu = mMenuManager.createContextMenu(this); |
| setMenu(menu); |
| |
| // Add listener to detect when the menu is about to be posted, such that |
| // we can sync the selection. Without this, you can right click on something |
| // in the canvas which is NOT selected, and the context menu will show items related |
| // to the selection, NOT the item you clicked on!! |
| addMenuDetectListener(new MenuDetectListener() { |
| @Override |
| public void menuDetected(MenuDetectEvent e) { |
| mSelectionManager.menuClick(e); |
| } |
| }); |
| } |
| |
| /** |
| * Invoked by {@link #createContextMenu()} to create our *static* context menu once. |
| * <p/> |
| * The content of the menu itself does not change. However the state of the |
| * various items is controlled by their associated actions. |
| * <p/> |
| * For cut/copy/paste/delete/select-all, we explicitly reuse the actions |
| * created by {@link #setupGlobalActionHandlers()}, so this method must be |
| * invoked after that one. |
| */ |
| private void setupStaticMenuActions(IMenuManager manager) { |
| manager.removeAll(); |
| |
| manager.add(new SelectionManager.SelectionMenu(getGraphicalEditor())); |
| manager.add(new Separator()); |
| manager.add(mCutAction); |
| manager.add(mCopyAction); |
| manager.add(mPasteAction); |
| manager.add(new Separator()); |
| manager.add(mDeleteAction); |
| manager.add(new Separator()); |
| manager.add(new PlayAnimationMenu(this)); |
| manager.add(new ExportScreenshotAction(this)); |
| manager.add(new Separator()); |
| |
| // Group "Show Included In" and "Show In" together |
| manager.add(new ShowWithinMenu(mEditorDelegate)); |
| |
| // Create a "Show In" sub-menu and automatically populate it using standard |
| // actions contributed by the workbench. |
| String showInLabel = IDEWorkbenchMessages.Workbench_showIn; |
| MenuManager showInSubMenu = new MenuManager(showInLabel); |
| showInSubMenu.add( |
| ContributionItemFactory.VIEWS_SHOW_IN.create( |
| mEditorDelegate.getEditor().getSite().getWorkbenchWindow())); |
| manager.add(showInSubMenu); |
| } |
| |
| /** |
| * Deletes the selection. Equivalent to pressing the Delete key. |
| */ |
| void delete() { |
| mDeleteAction.run(); |
| } |
| |
| /** |
| * Add new root in an existing empty XML layout. |
| * <p/> |
| * In case of error (unknown FQCN, document not empty), silently do nothing. |
| * In case of success, the new element will have some default attributes set |
| * (xmlns:android, layout_width and height). The edit is wrapped in a proper |
| * undo. |
| * <p/> |
| * This is invoked by |
| * {@link MoveGesture#drop(org.eclipse.swt.dnd.DropTargetEvent)}. |
| * |
| * @param root A non-null descriptor of the root element to create. |
| */ |
| void createDocumentRoot(final @NonNull SimpleElement root) { |
| String rootFqcn = root.getFqcn(); |
| |
| // Need a valid empty document to create the new root |
| final UiDocumentNode uiDoc = mEditorDelegate.getUiRootNode(); |
| if (uiDoc == null || uiDoc.getUiChildren().size() > 0) { |
| debugPrintf("Failed to create document root for %1$s: document is not empty", |
| rootFqcn); |
| return; |
| } |
| |
| // Find the view descriptor matching our FQCN |
| final ViewElementDescriptor viewDesc = mEditorDelegate.getFqcnViewDescriptor(rootFqcn); |
| if (viewDesc == null) { |
| // TODO this could happen if dropping a custom view not known in this project |
| debugPrintf("Failed to add document root, unknown FQCN %1$s", rootFqcn); |
| return; |
| } |
| |
| // Get the last segment of the FQCN for the undo title |
| String title = rootFqcn; |
| int pos = title.lastIndexOf('.'); |
| if (pos > 0 && pos < title.length() - 1) { |
| title = title.substring(pos + 1); |
| } |
| title = String.format("Create root %1$s in document", title); |
| |
| mEditorDelegate.getEditor().wrapUndoEditXmlModel(title, new Runnable() { |
| @Override |
| public void run() { |
| UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc); |
| |
| // A root node requires the Android XMLNS |
| uiNew.setAttributeValue( |
| SdkConstants.ANDROID_NS_NAME, |
| SdkConstants.XMLNS_URI, |
| SdkConstants.NS_RESOURCES, |
| true /*override*/); |
| |
| IDragAttribute[] attributes = root.getAttributes(); |
| if (attributes != null) { |
| for (IDragAttribute attribute : attributes) { |
| String uri = attribute.getUri(); |
| String name = attribute.getName(); |
| String value = attribute.getValue(); |
| uiNew.setAttributeValue(name, uri, value, false /*override*/); |
| } |
| } |
| |
| // Adjust the attributes |
| DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/); |
| |
| uiNew.createXmlNode(); |
| } |
| }); |
| } |
| |
| /** |
| * Returns the insets associated with views of the given fully qualified name, for the |
| * current theme and screen type. |
| * |
| * @param fqcn the fully qualified name to the widget type |
| * @return the insets, or null if unknown |
| */ |
| public Margins getInsets(String fqcn) { |
| if (ViewMetadataRepository.INSETS_SUPPORTED) { |
| ConfigurationChooser configComposite = getGraphicalEditor().getConfigurationChooser(); |
| String theme = configComposite.getThemeName(); |
| Density density = configComposite.getConfiguration().getDensity(); |
| return ViewMetadataRepository.getInsets(fqcn, density, theme); |
| } else { |
| return null; |
| } |
| } |
| |
| private void debugPrintf(String message, Object... params) { |
| if (DEBUG) { |
| AdtPlugin.printToConsole("Canvas", String.format(message, params)); |
| } |
| } |
| |
| /** The associated editor has been deactivated */ |
| public void deactivated() { |
| // Force the tooltip to be hidden. If you switch from the layout editor |
| // to a Java editor with the keyboard, the tooltip can stay open. |
| if (mLintTooltipManager != null) { |
| mLintTooltipManager.hide(); |
| } |
| } |
| |
| /** @see #setPreview(RenderPreview) */ |
| private RenderPreview mPreview; |
| |
| /** |
| * Sets the {@link RenderPreview} associated with the currently rendering |
| * configuration. |
| * <p> |
| * A {@link RenderPreview} has various additional state beyond its rendering, |
| * such as its display name (which can be edited by the user). When you click on |
| * previews, the layout editor switches to show the given configuration preview. |
| * The preview is then no longer shown in the list of previews and is instead rendered |
| * in the main editor. However, when you then switch away to some other preview, we |
| * want to be able to restore the preview with all its state. |
| * |
| * @param preview the preview associated with the current canvas |
| */ |
| public void setPreview(@Nullable RenderPreview preview) { |
| mPreview = preview; |
| } |
| |
| /** |
| * Returns the {@link RenderPreview} associated with this layout canvas. |
| * |
| * @see #setPreview(RenderPreview) |
| * @return the {@link RenderPreview} |
| */ |
| @Nullable |
| public RenderPreview getPreview() { |
| return mPreview; |
| } |
| |
| /** Ensures that the configuration previews are up to date for this canvas */ |
| public void syncPreviewMode() { |
| if (mImageOverlay != null && mImageOverlay.getImage() != null && |
| getGraphicalEditor().getConfigurationChooser().getResources() != null) { |
| if (mPreviewManager.recomputePreviews(false)) { |
| // Zoom when syncing modes |
| mZoomFitNextImage = true; |
| ensureZoomed(); |
| } |
| } |
| } |
| } |