blob: 814b82cec86251afa35ab29bceb0e51ce678967e [file] [log] [blame]
/*
* 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();
}
}
}
}