aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/AccordionControl.java396
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java352
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasAlternateSelection.java73
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java215
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java1178
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java429
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java195
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CreateNewConfigJob.java132
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CustomViewFinder.java395
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DelegatingAction.java203
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java915
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DropGesture.java87
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java654
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/EmptyViewsOverlay.java96
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExportScreenshotAction.java82
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/FragmentMenu.java304
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java645
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java156
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java930
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureToolTip.java217
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GlobalCanvasDragInfo.java182
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java2937
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/HoverOverlay.java187
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageControl.java241
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java447
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java979
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java1111
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java150
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java732
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java1720
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvasViewer.java165
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java413
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutPoint.java156
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutWindowCoordinator.java394
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java140
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltip.java94
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltipManager.java181
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java220
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MarqueeGesture.java160
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java852
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDragListener.java129
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDropListener.java217
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineOverlay.java107
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java1439
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Overlay.java91
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java1265
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PlayAnimationMenu.java247
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java642
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLogger.java327
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java1333
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewList.java222
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java1696
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewMode.java43
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java668
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java279
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandle.java141
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandles.java140
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java252
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java1262
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java247
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ShowWithinMenu.java82
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java124
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java370
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleXmlTransfer.java154
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SubmenuAction.java75
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java319
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java457
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java771
68 files changed, 32214 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/AccordionControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/AccordionControl.java
new file mode 100644
index 000000000..b3dce0756
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/AccordionControl.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright (C) 2011 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.ide.eclipse.adt.internal.editors.IconFactory;
+
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.ScrollBar;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The accordion control allows a series of labels with associated content that can be
+ * shown. For more details on accordions, see http://en.wikipedia.org/wiki/Accordion_(GUI)
+ * <p>
+ * This control allows the children to be created lazily. You can also customize the
+ * composite which is created to hold the children items, to for example allow multiple
+ * columns of items rather than just the default vertical stack.
+ * <p>
+ * The visual appearance of the headers is built in; it uses a mild gradient, with a
+ * heavier gradient during mouse-overs. It also uses a bold label along with the eclipse
+ * folder icons.
+ * <p>
+ * The control can be configured to enforce a single category open at any time (the
+ * default), or allowing multiple categories open (where they share the available space).
+ * The control can also be configured to fill the available vertical space for the open
+ * category/categories.
+ */
+public abstract class AccordionControl extends Composite {
+ /** Pixel spacing between header items */
+ private static final int HEADER_SPACING = 0;
+
+ /** Pixel spacing between items in the content area */
+ private static final int ITEM_SPACING = 0;
+
+ private static final String KEY_CONTENT = "content"; //$NON-NLS-1$
+ private static final String KEY_HEADER = "header"; //$NON-NLS-1$
+
+ private Image mClosed;
+ private Image mOpen;
+ private boolean mSingle = true;
+ private boolean mWrap;
+
+ /**
+ * Creates the container which will hold the items in a category; this can be
+ * overridden to lay out the children with a different layout than the default
+ * vertical RowLayout
+ */
+ protected Composite createChildContainer(Composite parent, Object header, int style) {
+ Composite composite = new Composite(parent, style);
+ if (mWrap) {
+ RowLayout layout = new RowLayout(SWT.HORIZONTAL);
+ layout.center = true;
+ composite.setLayout(layout);
+ } else {
+ RowLayout layout = new RowLayout(SWT.VERTICAL);
+ layout.spacing = ITEM_SPACING;
+ layout.marginHeight = 0;
+ layout.marginWidth = 0;
+ layout.marginLeft = 0;
+ layout.marginTop = 0;
+ layout.marginRight = 0;
+ layout.marginBottom = 0;
+ composite.setLayout(layout);
+ }
+
+ // TODO - maybe do multi-column arrangement for simple nodes
+ return composite;
+ }
+
+ /**
+ * Creates the children under a particular header
+ *
+ * @param parent the parent composite to add the SWT items to
+ * @param header the header object that is being opened for the first time
+ */
+ protected abstract void createChildren(Composite parent, Object header);
+
+ /**
+ * Set whether a single category should be enforced or not (default=true)
+ *
+ * @param single if true, enforce a single category open at a time
+ */
+ public void setAutoClose(boolean single) {
+ mSingle = single;
+ }
+
+ /**
+ * Returns whether a single category should be enforced or not (default=true)
+ *
+ * @return true if only a single category can be open at a time
+ */
+ public boolean isAutoClose() {
+ return mSingle;
+ }
+
+ /**
+ * Returns the labels used as header categories
+ *
+ * @return list of header labels
+ */
+ public List<CLabel> getHeaderLabels() {
+ List<CLabel> headers = new ArrayList<CLabel>();
+ for (Control c : getChildren()) {
+ if (c instanceof CLabel) {
+ headers.add((CLabel) c);
+ }
+ }
+
+ return headers;
+ }
+
+ /**
+ * Show all categories
+ *
+ * @param performLayout if true, call {@link #layout} and {@link #pack} when done
+ */
+ public void expandAll(boolean performLayout) {
+ for (Control c : getChildren()) {
+ if (c instanceof CLabel) {
+ if (!isOpen(c)) {
+ toggle((CLabel) c, false, false);
+ }
+ }
+ }
+ if (performLayout) {
+ pack();
+ layout();
+ }
+ }
+
+ /**
+ * Hide all categories
+ *
+ * @param performLayout if true, call {@link #layout} and {@link #pack} when done
+ */
+ public void collapseAll(boolean performLayout) {
+ for (Control c : getChildren()) {
+ if (c instanceof CLabel) {
+ if (isOpen(c)) {
+ toggle((CLabel) c, false, false);
+ }
+ }
+ }
+ if (performLayout) {
+ layout();
+ }
+ }
+
+ /**
+ * Create the composite.
+ *
+ * @param parent the parent widget to add the accordion to
+ * @param style the SWT style mask to use
+ * @param headers a list of headers, whose {@link Object#toString} method should
+ * produce the heading label
+ * @param greedy if true, grow vertically as much as possible
+ * @param wrapChildren if true, configure the child area to be horizontally laid out
+ * with wrapping
+ * @param expand Set of headers to expand initially
+ */
+ public AccordionControl(Composite parent, int style, List<?> headers,
+ boolean greedy, boolean wrapChildren, Set<String> expand) {
+ super(parent, style);
+ mWrap = wrapChildren;
+
+ GridLayout gridLayout = new GridLayout(1, false);
+ gridLayout.verticalSpacing = HEADER_SPACING;
+ gridLayout.horizontalSpacing = 0;
+ gridLayout.marginWidth = 0;
+ gridLayout.marginHeight = 0;
+ setLayout(gridLayout);
+
+ Font labelFont = null;
+
+ mOpen = IconFactory.getInstance().getIcon("open-folder"); //$NON-NLS-1$
+ mClosed = IconFactory.getInstance().getIcon("closed-folder"); //$NON-NLS-1$
+ List<CLabel> expandLabels = new ArrayList<CLabel>();
+
+ for (Object header : headers) {
+ final CLabel label = new CLabel(this, SWT.SHADOW_OUT);
+ label.setText(header.toString().replace("&", "&&")); //$NON-NLS-1$ //$NON-NLS-2$
+ updateBackground(label, false);
+ if (labelFont == null) {
+ labelFont = JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT);
+ }
+ label.setFont(labelFont);
+ label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ setHeader(header, label);
+ label.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseUp(MouseEvent e) {
+ if (e.button == 1 && (e.stateMask & SWT.MODIFIER_MASK) == 0) {
+ toggle(label, true, mSingle);
+ }
+ }
+ });
+ label.addMouseTrackListener(new MouseTrackListener() {
+ @Override
+ public void mouseEnter(MouseEvent e) {
+ updateBackground(label, true);
+ }
+
+ @Override
+ public void mouseExit(MouseEvent e) {
+ updateBackground(label, false);
+ }
+
+ @Override
+ public void mouseHover(MouseEvent e) {
+ }
+ });
+
+ // Turn off border?
+ final ScrolledComposite scrolledComposite = new ScrolledComposite(this, SWT.V_SCROLL);
+ ScrollBar verticalBar = scrolledComposite.getVerticalBar();
+ verticalBar.setIncrement(20);
+ verticalBar.setPageIncrement(100);
+
+ // Do we need the scrolled composite or can we just look at the next
+ // wizard in the hierarchy?
+
+ setContentArea(label, scrolledComposite);
+ scrolledComposite.setExpandHorizontal(true);
+ scrolledComposite.setExpandVertical(true);
+ GridData scrollGridData = new GridData(SWT.FILL,
+ greedy ? SWT.FILL : SWT.TOP, false, greedy, 1, 1);
+ scrollGridData.exclude = true;
+ scrollGridData.grabExcessHorizontalSpace = wrapChildren;
+ scrolledComposite.setLayoutData(scrollGridData);
+
+ if (wrapChildren) {
+ scrolledComposite.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Rectangle r = scrolledComposite.getClientArea();
+ Control content = scrolledComposite.getContent();
+ if (content != null && r != null) {
+ Point minSize = content.computeSize(r.width, SWT.DEFAULT);
+ scrolledComposite.setMinSize(minSize);
+ ScrollBar vBar = scrolledComposite.getVerticalBar();
+ vBar.setPageIncrement(r.height);
+ }
+ }
+ });
+ }
+
+ updateIcon(label);
+ if (expand != null && expand.contains(label.getText())) {
+ // Comparing "label.getText()" rather than "header" because we make some
+ // tweaks to the label (replacing & with && etc) and in the getExpandedCategories
+ // method we return the label texts
+ expandLabels.add(label);
+ }
+ }
+
+ // Expand the requested categories
+ for (CLabel label : expandLabels) {
+ toggle(label, false, false);
+ }
+ }
+
+ /** Updates the background gradient of the given header label */
+ private void updateBackground(CLabel label, boolean mouseOver) {
+ Display display = label.getDisplay();
+ label.setBackground(new Color[] {
+ display.getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW),
+ display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND),
+ display.getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW)
+ }, new int[] {
+ mouseOver ? 60 : 40, 100
+ }, true);
+ }
+
+ /**
+ * Updates the icon for a header label to be open/close based on the {@link #isOpen}
+ * state
+ */
+ private void updateIcon(CLabel label) {
+ label.setImage(isOpen(label) ? mOpen : mClosed);
+ }
+
+ /** Returns true if the content area for the given label is open/showing */
+ private boolean isOpen(Control label) {
+ return !((GridData) getContentArea(label).getLayoutData()).exclude;
+ }
+
+ /** Toggles the visibility of the children of the given label */
+ private void toggle(CLabel label, boolean performLayout, boolean autoClose) {
+ if (autoClose) {
+ collapseAll(true);
+ }
+ ScrolledComposite scrolledComposite = getContentArea(label);
+
+ GridData scrollGridData = (GridData) scrolledComposite.getLayoutData();
+ boolean close = !scrollGridData.exclude;
+ scrollGridData.exclude = close;
+ scrolledComposite.setVisible(!close);
+ updateIcon(label);
+
+ if (!scrollGridData.exclude && scrolledComposite.getContent() == null) {
+ Object header = getHeader(label);
+ Composite composite = createChildContainer(scrolledComposite, header, SWT.NONE);
+ createChildren(composite, header);
+ while (composite.getParent() != scrolledComposite) {
+ composite = composite.getParent();
+ }
+ scrolledComposite.setContent(composite);
+ scrolledComposite.setMinSize(composite.computeSize(SWT.DEFAULT, SWT.DEFAULT));
+ }
+
+ if (performLayout) {
+ layout(true);
+ }
+ }
+
+ /** Returns the header object for the given header label */
+ private Object getHeader(Control label) {
+ return label.getData(KEY_HEADER);
+ }
+
+ /** Sets the header object for the given header label */
+ private void setHeader(Object header, final CLabel label) {
+ label.setData(KEY_HEADER, header);
+ }
+
+ /** Returns the content area for the given header label */
+ private ScrolledComposite getContentArea(Control label) {
+ return (ScrolledComposite) label.getData(KEY_CONTENT);
+ }
+
+ /** Sets the content area for the given header label */
+ private void setContentArea(final CLabel label, ScrolledComposite scrolledComposite) {
+ label.setData(KEY_CONTENT, scrolledComposite);
+ }
+
+ @Override
+ protected void checkSubclass() {
+ // Disable the check that prevents subclassing of SWT components
+ }
+
+ /**
+ * Returns the set of expanded categories in the palette. Note: Header labels will have
+ * escaped ampersand characters with double ampersands.
+ *
+ * @return the set of expanded categories in the palette - never null
+ */
+ public Set<String> getExpandedCategories() {
+ Set<String> expanded = new HashSet<String>();
+ for (Control c : getChildren()) {
+ if (c instanceof CLabel) {
+ if (isOpen(c)) {
+ expanded.add(((CLabel) c).getText());
+ }
+ }
+ }
+
+ return expanded;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java
new file mode 100644
index 000000000..9fc2e0937
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2012 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.annotations.Nullable;
+import com.android.ide.common.api.Rect;
+
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.imageio.ImageIO;
+
+/**
+ * This class implements 2D bin packing: packing rectangles into a given area as
+ * tightly as "possible" (bin packing in general is NP hard, so this class uses
+ * heuristics).
+ * <p>
+ * The algorithm implemented is to keep a set of (possibly overlapping)
+ * available areas for placement. For each newly inserted rectangle, we first
+ * pick which available space to occupy, and we then subdivide the
+ * current rectangle into all the possible remaining unoccupied sub-rectangles.
+ * We also remove any other space rectangles which are no longer eligible if
+ * they are intersecting the newly placed rectangle.
+ * <p>
+ * This algorithm is not very fast, so should not be used for a large number of
+ * rectangles.
+ */
+class BinPacker {
+ /**
+ * When enabled, the successive passes are dumped as PNG images showing the
+ * various available and occupied rectangles)
+ */
+ private static final boolean DEBUG = false;
+
+ private final List<Rect> mSpace = new ArrayList<Rect>();
+ private final int mMinHeight;
+ private final int mMinWidth;
+
+ /**
+ * Creates a new {@linkplain BinPacker}. To use it, first add one or more
+ * initial available space rectangles with {@link #addSpace(Rect)}, and then
+ * place the rectangles with {@link #occupy(int, int)}. The returned
+ * {@link Rect} from {@link #occupy(int, int)} gives the coordinates of the
+ * positioned rectangle.
+ *
+ * @param minWidth the smallest width of any rectangle placed into this bin
+ * @param minHeight the smallest height of any rectangle placed into this bin
+ */
+ BinPacker(int minWidth, int minHeight) {
+ mMinWidth = minWidth;
+ mMinHeight = minHeight;
+
+ if (DEBUG) {
+ mAllocated = new ArrayList<Rect>();
+ sLayoutId++;
+ sRectId = 1;
+ }
+ }
+
+ /** Adds more available space */
+ void addSpace(Rect rect) {
+ if (rect.w >= mMinWidth && rect.h >= mMinHeight) {
+ mSpace.add(rect);
+ }
+ }
+
+ /** Attempts to place a rectangle of the given dimensions, if possible */
+ @Nullable
+ Rect occupy(int width, int height) {
+ int index = findBest(width, height);
+ if (index == -1) {
+ return null;
+ }
+
+ return split(index, width, height);
+ }
+
+ /**
+ * Finds the best available space rectangle to position a new rectangle of
+ * the given size in.
+ */
+ private int findBest(int width, int height) {
+ if (mSpace.isEmpty()) {
+ return -1;
+ }
+
+ // Try to pack as far up as possible first
+ int bestIndex = -1;
+ boolean multipleAtSameY = false;
+ int minY = Integer.MAX_VALUE;
+ for (int i = 0, n = mSpace.size(); i < n; i++) {
+ Rect rect = mSpace.get(i);
+ if (rect.y <= minY) {
+ if (rect.w >= width && rect.h >= height) {
+ if (rect.y < minY) {
+ minY = rect.y;
+ multipleAtSameY = false;
+ bestIndex = i;
+ } else if (minY == rect.y) {
+ multipleAtSameY = true;
+ }
+ }
+ }
+ }
+
+ if (!multipleAtSameY) {
+ return bestIndex;
+ }
+
+ bestIndex = -1;
+
+ // Pick a rectangle. This currently tries to find the rectangle whose shortest
+ // side most closely matches the placed rectangle's size.
+ // Attempt to find the best short side fit
+ int bestShortDistance = Integer.MAX_VALUE;
+ int bestLongDistance = Integer.MAX_VALUE;
+
+ for (int i = 0, n = mSpace.size(); i < n; i++) {
+ Rect rect = mSpace.get(i);
+ if (rect.y != minY) { // Only comparing elements at same y
+ continue;
+ }
+ if (rect.w >= width && rect.h >= height) {
+ if (width < height) {
+ int distance = rect.w - width;
+ if (distance < bestShortDistance ||
+ distance == bestShortDistance &&
+ (rect.h - height) < bestLongDistance) {
+ bestShortDistance = distance;
+ bestLongDistance = rect.h - height;
+ bestIndex = i;
+ }
+ } else {
+ int distance = rect.w - width;
+ if (distance < bestShortDistance ||
+ distance == bestShortDistance &&
+ (rect.h - height) < bestLongDistance) {
+ bestShortDistance = distance;
+ bestLongDistance = rect.h - height;
+ bestIndex = i;
+ }
+ }
+ }
+ }
+
+ return bestIndex;
+ }
+
+ /**
+ * Removes the rectangle at the given index. Since the rectangles are in an
+ * {@link ArrayList}, removing a rectangle in the normal way is slow (it
+ * would involve shifting all elements), but since we don't care about
+ * order, this always swaps the to-be-deleted element to the last position
+ * in the array first, <b>then</b> it deletes it (which should be
+ * immediate).
+ *
+ * @param index the index in the {@link #mSpace} list to remove a rectangle
+ * from
+ */
+ private void removeRect(int index) {
+ assert !mSpace.isEmpty();
+ int lastIndex = mSpace.size() - 1;
+ if (index != lastIndex) {
+ // Swap before remove to make deletion faster since we don't
+ // care about order
+ Rect temp = mSpace.get(index);
+ mSpace.set(index, mSpace.get(lastIndex));
+ mSpace.set(lastIndex, temp);
+ }
+
+ mSpace.remove(lastIndex);
+ }
+
+ /**
+ * Splits the rectangle at the given rectangle index such that it can contain
+ * a rectangle of the given width and height. */
+ private Rect split(int index, int width, int height) {
+ Rect rect = mSpace.get(index);
+ assert rect.w >= width && rect.h >= height : rect;
+
+ Rect r = new Rect(rect);
+ r.w = width;
+ r.h = height;
+
+ // Remove all rectangles that intersect my rectangle
+ for (int i = 0; i < mSpace.size(); i++) {
+ Rect other = mSpace.get(i);
+ if (other.intersects(r)) {
+ removeRect(i);
+ i--;
+ }
+ }
+
+
+ // Split along vertical line x = rect.x + width:
+ // (rect.x,rect.y)
+ // +-------------+-------------------------+
+ // | | |
+ // | | |
+ // | | height |
+ // | | |
+ // | | |
+ // +-------------+ B | rect.h
+ // | width |
+ // | | |
+ // | A |
+ // | | |
+ // | |
+ // +---------------------------------------+
+ // rect.w
+ int remainingHeight = rect.h - height;
+ int remainingWidth = rect.w - width;
+ if (remainingHeight >= mMinHeight) {
+ mSpace.add(new Rect(rect.x, rect.y + height, width, remainingHeight));
+ }
+ if (remainingWidth >= mMinWidth) {
+ mSpace.add(new Rect(rect.x + width, rect.y, remainingWidth, rect.h));
+ }
+
+ // Split along horizontal line y = rect.y + height:
+ // +-------------+-------------------------+
+ // | | |
+ // | | height |
+ // | | A |
+ // | | |
+ // | | | rect.h
+ // +-------------+ - - - - - - - - - - - - |
+ // | width |
+ // | |
+ // | B |
+ // | |
+ // | |
+ // +---------------------------------------+
+ // rect.w
+ if (remainingHeight >= mMinHeight) {
+ mSpace.add(new Rect(rect.x, rect.y + height, rect.w, remainingHeight));
+ }
+ if (remainingWidth >= mMinWidth) {
+ mSpace.add(new Rect(rect.x + width, rect.y, remainingWidth, height));
+ }
+
+ // Remove redundant rectangles. This is not very efficient.
+ for (int i = 0; i < mSpace.size() - 1; i++) {
+ for (int j = i + 1; j < mSpace.size(); j++) {
+ Rect iRect = mSpace.get(i);
+ Rect jRect = mSpace.get(j);
+ if (jRect.contains(iRect)) {
+ removeRect(i);
+ i--;
+ break;
+ }
+ if (iRect.contains(jRect)) {
+ removeRect(j);
+ j--;
+ }
+ }
+ }
+
+ if (DEBUG) {
+ mAllocated.add(r);
+ dumpImage();
+ }
+
+ return r;
+ }
+
+ // DEBUGGING CODE: Enable with DEBUG
+
+ private List<Rect> mAllocated;
+ private static int sLayoutId;
+ private static int sRectId;
+ private void dumpImage() {
+ if (DEBUG) {
+ int width = 100;
+ int height = 100;
+ for (Rect rect : mSpace) {
+ width = Math.max(width, rect.w);
+ height = Math.max(height, rect.h);
+ }
+ width += 10;
+ height += 10;
+
+ BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ Graphics2D g = image.createGraphics();
+ g.setColor(Color.BLACK);
+ g.fillRect(0, 0, image.getWidth(), image.getHeight());
+
+ Color[] colors = new Color[] {
+ Color.blue, Color.cyan,
+ Color.green, Color.magenta, Color.orange,
+ Color.pink, Color.red, Color.white, Color.yellow, Color.darkGray,
+ Color.lightGray, Color.gray,
+ };
+
+ char allocated = 'A';
+ for (Rect rect : mAllocated) {
+ Color color = new Color(0x9FFFFFFF, true);
+ g.setColor(color);
+ g.setBackground(color);
+ g.fillRect(rect.x, rect.y, rect.w, rect.h);
+ g.setColor(Color.WHITE);
+ g.drawRect(rect.x, rect.y, rect.w, rect.h);
+ g.drawString("" + (allocated++),
+ rect.x + rect.w / 2, rect.y + rect.h / 2);
+ }
+
+ int colorIndex = 0;
+ for (Rect rect : mSpace) {
+ Color color = colors[colorIndex];
+ colorIndex = (colorIndex + 1) % colors.length;
+
+ color = new Color(color.getRed(), color.getGreen(), color.getBlue(), 128);
+ g.setColor(color);
+
+ g.fillRect(rect.x, rect.y, rect.w, rect.h);
+ g.setColor(Color.WHITE);
+ g.drawString(Integer.toString(colorIndex),
+ rect.x + rect.w / 2, rect.y + rect.h / 2);
+ }
+
+
+ g.dispose();
+
+ File file = new File("/tmp/layout" + sLayoutId + "_pass" + sRectId + ".png");
+ try {
+ ImageIO.write(image, "PNG", file);
+ System.out.println("Wrote diagnostics image " + file);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ sRectId++;
+ }
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasAlternateSelection.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasAlternateSelection.java
new file mode 100644
index 000000000..c04061cbd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasAlternateSelection.java
@@ -0,0 +1,73 @@
+/*
+ * 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 java.util.List;
+
+/**
+ * Information for the current alternate selection, i.e. the possible selected items
+ * that are located at the same x/y as the original view, either sibling or parents.
+ */
+/* package */ class CanvasAlternateSelection {
+ private final CanvasViewInfo mOriginatingView;
+ private final List<CanvasViewInfo> mAltViews;
+ private int mIndex;
+
+ /**
+ * Creates a new alternate selection based on the given originating view and the
+ * given list of alternate views. Both cannot be null.
+ */
+ public CanvasAlternateSelection(CanvasViewInfo originatingView, List<CanvasViewInfo> altViews) {
+ assert originatingView != null;
+ assert altViews != null;
+ mOriginatingView = originatingView;
+ mAltViews = altViews;
+ mIndex = altViews.size() - 1;
+ }
+
+ /** Returns the list of alternate views. Cannot be null. */
+ public List<CanvasViewInfo> getAltViews() {
+ return mAltViews;
+ }
+
+ /** Returns the originating view. Cannot be null. */
+ public CanvasViewInfo getOriginatingView() {
+ return mOriginatingView;
+ }
+
+ /**
+ * Returns the current alternate view to select.
+ * Initially this is the top-most view.
+ */
+ public CanvasViewInfo getCurrent() {
+ return mIndex >= 0 ? mAltViews.get(mIndex) : null;
+ }
+
+ /**
+ * Changes the current view to be the next one and then returns it.
+ * This loops through the alternate views.
+ */
+ public CanvasViewInfo getNext() {
+ if (mIndex == 0) {
+ mIndex = mAltViews.size() - 1;
+ } else if (mIndex > 0) {
+ mIndex--;
+ }
+
+ return getCurrent();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java
new file mode 100644
index 000000000..ad5bd52e5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java
@@ -0,0 +1,215 @@
+/*
+ * 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 static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE;
+
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.ScrollBar;
+
+/**
+ * Helper class to convert between control pixel coordinates and canvas coordinates.
+ * Takes care of the zooming and offset of the canvas.
+ */
+public class CanvasTransform {
+ /**
+ * Default margin around the rendered image, reduced
+ * when the contents do not fit.
+ */
+ public static final int DEFAULT_MARGIN = 25;
+
+ /**
+ * The canvas which controls the zooming.
+ */
+ private final LayoutCanvas mCanvas;
+
+ /** Canvas image size (original, before zoom), in pixels. */
+ private int mImgSize;
+
+ /** Full size being scrolled (after zoom), in pixels */
+ private int mFullSize;;
+
+ /** Client size, in pixels. */
+ private int mClientSize;
+
+ /** Left-top offset in client pixel coordinates. */
+ private int mTranslate;
+
+ /** Current margin */
+ private int mMargin = DEFAULT_MARGIN;
+
+ /** Scaling factor, > 0. */
+ private double mScale;
+
+ /** Scrollbar widget. */
+ private ScrollBar mScrollbar;
+
+ public CanvasTransform(LayoutCanvas layoutCanvas, ScrollBar scrollbar) {
+ mCanvas = layoutCanvas;
+ mScrollbar = scrollbar;
+ mScale = 1.0;
+ mTranslate = 0;
+
+ mScrollbar.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // User requested scrolling. Changes translation and redraw canvas.
+ mTranslate = mScrollbar.getSelection();
+ CanvasTransform.this.mCanvas.redraw();
+ }
+ });
+ mScrollbar.setIncrement(20);
+ }
+
+ /**
+ * Sets the new scaling factor. Recomputes scrollbars.
+ * @param scale Scaling factor, > 0.
+ */
+ public void setScale(double scale) {
+ if (mScale != scale) {
+ mScale = scale;
+ resizeScrollbar();
+ }
+ }
+
+ /** Recomputes the scrollbar and view port settings */
+ public void refresh() {
+ resizeScrollbar();
+ }
+
+ /**
+ * Returns current scaling factor.
+ *
+ * @return The current scaling factor
+ */
+ public double getScale() {
+ return mScale;
+ }
+
+ /**
+ * Returns Canvas image size (original, before zoom), in pixels.
+ *
+ * @return Canvas image size (original, before zoom), in pixels
+ */
+ public int getImgSize() {
+ return mImgSize;
+ }
+
+ /**
+ * Returns the scaled image size in pixels.
+ *
+ * @return The scaled image size in pixels.
+ */
+ public int getScaledImgSize() {
+ return (int) (mImgSize * mScale);
+ }
+
+ /**
+ * Changes the size of the canvas image and the client size. Recomputes
+ * scrollbars.
+ *
+ * @param imgSize the size of the image being scaled
+ * @param fullSize the size of the full view area being scrolled
+ * @param clientSize the size of the view port
+ */
+ public void setSize(int imgSize, int fullSize, int clientSize) {
+ mImgSize = imgSize;
+ mFullSize = fullSize;
+ mClientSize = clientSize;
+ mScrollbar.setPageIncrement(clientSize);
+ resizeScrollbar();
+ }
+
+ private void resizeScrollbar() {
+ // scaled image size
+ int sx = (int) (mScale * mFullSize);
+
+ // Adjust margin such that for zoomed out views
+ // we don't waste space (unless the viewport is
+ // large enough to accommodate it)
+ int delta = mClientSize - sx;
+ if (delta < 0) {
+ mMargin = 0;
+ } else if (delta < 2 * DEFAULT_MARGIN) {
+ mMargin = delta / 2;
+
+ ImageOverlay imageOverlay = mCanvas.getImageOverlay();
+ if (imageOverlay != null && imageOverlay.getShowDropShadow()
+ && delta >= SHADOW_SIZE / 2) {
+ mMargin -= SHADOW_SIZE / 2;
+ // Add a little padding on the top too, if there's room. The shadow assets
+ // include enough padding on the bottom to not make this look clipped.
+ if (mMargin < 4) {
+ mMargin += 4;
+ }
+ }
+ } else {
+ mMargin = DEFAULT_MARGIN;
+ }
+
+ if (mCanvas.getPreviewManager().hasPreviews()) {
+ // Make more room for the previews
+ mMargin = 2;
+ }
+
+ // actual client area is always reduced by the margins
+ int cx = mClientSize - 2 * mMargin;
+
+ if (sx < cx) {
+ mTranslate = 0;
+ mScrollbar.setEnabled(false);
+ } else {
+ mScrollbar.setEnabled(true);
+
+ int selection = mScrollbar.getSelection();
+ int thumb = cx;
+ int maximum = sx;
+
+ if (selection + thumb > maximum) {
+ selection = maximum - thumb;
+ if (selection < 0) {
+ selection = 0;
+ }
+ }
+
+ mScrollbar.setValues(selection, mScrollbar.getMinimum(), maximum, thumb, mScrollbar
+ .getIncrement(), mScrollbar.getPageIncrement());
+
+ mTranslate = selection;
+ }
+ }
+
+ public int getMargin() {
+ return mMargin;
+ }
+
+ public int translate(int canvasX) {
+ return mMargin - mTranslate + (int) (mScale * canvasX);
+ }
+
+ public int scale(int canwasW) {
+ return (int) (mScale * canwasW);
+ }
+
+ public int inverseTranslate(int screenX) {
+ return (int) ((screenX - mMargin + mTranslate) / mScale);
+ }
+
+ public int inverseScale(int canwasW) {
+ return (int) (canwasW / mScale);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java
new file mode 100644
index 000000000..03c6c3926
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java
@@ -0,0 +1,1178 @@
+/*
+ * 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 static com.android.SdkConstants.FQCN_SPACE;
+import static com.android.SdkConstants.FQCN_SPACE_V7;
+import static com.android.SdkConstants.GESTURE_OVERLAY_VIEW;
+import static com.android.SdkConstants.VIEW_MERGE;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.layout.GridLayoutRule;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.rendering.api.MergeCookie;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.utils.Pair;
+
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.ui.views.properties.IPropertyDescriptor;
+import org.eclipse.ui.views.properties.IPropertySheetPage;
+import org.eclipse.ui.views.properties.IPropertySource;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Maps a {@link ViewInfo} in a structure more adapted to our needs.
+ * The only large difference is that we keep both the original bounds of the view info
+ * and we pre-compute the selection bounds which are absolute to the rendered image
+ * (whereas the original bounds are relative to the parent view.)
+ * <p/>
+ * Each view also knows its parent and children.
+ * <p/>
+ * We can't alter {@link ViewInfo} as it is part of the LayoutBridge and needs to
+ * have a fixed API.
+ * <p/>
+ * The view info also implements {@link IPropertySource}, which enables a linked
+ * {@link IPropertySheetPage} to display the attributes of the selected element.
+ * This class actually delegates handling of {@link IPropertySource} to the underlying
+ * {@link UiViewElementNode}, if any.
+ */
+public class CanvasViewInfo implements IPropertySource {
+
+ /**
+ * Minimal size of the selection, in case an empty view or layout is selected.
+ */
+ public static final int SELECTION_MIN_SIZE = 6;
+
+ private final Rectangle mAbsRect;
+ private final Rectangle mSelectionRect;
+ private final String mName;
+ private final Object mViewObject;
+ private final UiViewElementNode mUiViewNode;
+ private CanvasViewInfo mParent;
+ private ViewInfo mViewInfo;
+ private final List<CanvasViewInfo> mChildren = new ArrayList<CanvasViewInfo>();
+
+ /**
+ * Is this view info an individually exploded view? This is the case for views
+ * that were specially inflated by the {@link UiElementPullParser} and assigned
+ * fixed padding because they were invisible and somebody requested visibility.
+ */
+ private boolean mExploded;
+
+ /**
+ * Node sibling. This is usually null, but it's possible for a single node in the
+ * model to have <b>multiple</b> separate views in the canvas, for example
+ * when you {@code <include>} a view that has multiple widgets inside a
+ * {@code <merge>} tag. In this case, all the views have the same node model,
+ * the include tag, and selecting the include should highlight all the separate
+ * views that are linked to this node. That's what this field is all about: it is
+ * a <b>circular</b> list of all the siblings that share the same node.
+ */
+ private List<CanvasViewInfo> mNodeSiblings;
+
+ /**
+ * Constructs a {@link CanvasViewInfo} initialized with the given initial values.
+ */
+ private CanvasViewInfo(CanvasViewInfo parent, String name,
+ Object viewObject, UiViewElementNode node, Rectangle absRect,
+ Rectangle selectionRect, ViewInfo viewInfo) {
+ mParent = parent;
+ mName = name;
+ mViewObject = viewObject;
+ mViewInfo = viewInfo;
+ mUiViewNode = node;
+ mAbsRect = absRect;
+ mSelectionRect = selectionRect;
+ }
+
+ /**
+ * Returns the original {@link ViewInfo} bounds in absolute coordinates
+ * over the whole graphic.
+ *
+ * @return the bounding box in absolute coordinates
+ */
+ @NonNull
+ public Rectangle getAbsRect() {
+ return mAbsRect;
+ }
+
+ /**
+ * Returns the absolute selection bounds of the view info as a rectangle.
+ * The selection bounds will always have a size greater or equal to
+ * {@link #SELECTION_MIN_SIZE}.
+ * The width/height is inclusive (i.e. width = right-left-1).
+ * This is in absolute "screen" coordinates (relative to the rendered bitmap).
+ *
+ * @return the absolute selection bounds
+ */
+ @NonNull
+ public Rectangle getSelectionRect() {
+ return mSelectionRect;
+ }
+
+ /**
+ * Returns the view node. Could be null, although unlikely.
+ * @return An {@link UiViewElementNode} that uniquely identifies the object in the XML model.
+ * @see ViewInfo#getCookie()
+ */
+ @Nullable
+ public UiViewElementNode getUiViewNode() {
+ return mUiViewNode;
+ }
+
+ /**
+ * Returns the parent {@link CanvasViewInfo}.
+ * It is null for the root and non-null for children.
+ *
+ * @return the parent {@link CanvasViewInfo}, which can be null
+ */
+ @Nullable
+ public CanvasViewInfo getParent() {
+ return mParent;
+ }
+
+ /**
+ * Returns the list of children of this {@link CanvasViewInfo}.
+ * The list is never null. It can be empty.
+ * By contract, this.getChildren().get(0..n-1).getParent() == this.
+ *
+ * @return the children, never null
+ */
+ @NonNull
+ public List<CanvasViewInfo> getChildren() {
+ return mChildren;
+ }
+
+ /**
+ * For nodes that have multiple views rendered from a single node, such as the
+ * children of a {@code <merge>} tag included into a separate layout, return the
+ * "primary" view, the first view that is rendered
+ */
+ @Nullable
+ private CanvasViewInfo getPrimaryNodeSibling() {
+ if (mNodeSiblings == null || mNodeSiblings.size() == 0) {
+ return null;
+ }
+
+ return mNodeSiblings.get(0);
+ }
+
+ /**
+ * Returns true if this view represents one view of many linked to a single node, and
+ * where this is the primary view. The primary view is the one that will be shown
+ * in the outline for example (since we only show nodes, not views, in the outline,
+ * and therefore don't want repetitions when a view has more than one view info.)
+ *
+ * @return true if this is the primary view among more than one linked to a single
+ * node
+ */
+ private boolean isPrimaryNodeSibling() {
+ return getPrimaryNodeSibling() == this;
+ }
+
+ /**
+ * Returns the list of node sibling of this view (which <b>will include this
+ * view</b>). For most views this is going to be null, but for views that share a
+ * single node (such as widgets inside a {@code <merge>} tag included into another
+ * layout), this will provide all the views that correspond to the node.
+ *
+ * @return a non-empty list of siblings (including this), or null
+ */
+ @Nullable
+ public List<CanvasViewInfo> getNodeSiblings() {
+ return mNodeSiblings;
+ }
+
+ /**
+ * Returns all the children of the canvas view info where each child corresponds to a
+ * unique node that the user can see and select. This is intended for use by the
+ * outline for example, where only the actual nodes are displayed, not the views
+ * themselves.
+ * <p>
+ * Most views have their own nodes, so this is generally the same as
+ * {@link #getChildren}, except in the case where you for example include a view that
+ * has multiple widgets inside a {@code <merge>} tag, where all these widgets have the
+ * same node (the {@code <merge>} tag).
+ *
+ * @return list of {@link CanvasViewInfo} objects that are children of this view,
+ * never null
+ */
+ @NonNull
+ public List<CanvasViewInfo> getUniqueChildren() {
+ boolean haveHidden = false;
+
+ for (CanvasViewInfo info : mChildren) {
+ if (info.mNodeSiblings != null) {
+ // We have secondary children; must create a new collection containing
+ // only non-secondary children
+ List<CanvasViewInfo> children = new ArrayList<CanvasViewInfo>();
+ for (CanvasViewInfo vi : mChildren) {
+ if (vi.mNodeSiblings == null) {
+ children.add(vi);
+ } else if (vi.isPrimaryNodeSibling()) {
+ children.add(vi);
+ }
+ }
+ return children;
+ }
+
+ haveHidden |= info.isHidden();
+ }
+
+ if (haveHidden) {
+ List<CanvasViewInfo> children = new ArrayList<CanvasViewInfo>(mChildren.size());
+ for (CanvasViewInfo vi : mChildren) {
+ if (!vi.isHidden()) {
+ children.add(vi);
+ }
+ }
+
+ return children;
+ }
+
+ return mChildren;
+ }
+
+ /**
+ * Returns true if the specific {@link CanvasViewInfo} is a parent
+ * of this {@link CanvasViewInfo}. It can be a direct parent or any
+ * grand-parent higher in the hierarchy.
+ *
+ * @param potentialParent the view info to check
+ * @return true if the given info is a parent of this view
+ */
+ public boolean isParent(@NonNull CanvasViewInfo potentialParent) {
+ CanvasViewInfo p = mParent;
+ while (p != null) {
+ if (p == potentialParent) {
+ return true;
+ }
+ p = p.getParent();
+ }
+ return false;
+ }
+
+ /**
+ * Returns the name of the {@link CanvasViewInfo}.
+ * Could be null, although unlikely.
+ * Experience shows this is the full qualified Java name of the View.
+ * TODO: Rename this method to getFqcn.
+ *
+ * @return the name of the view info
+ *
+ * @see ViewInfo#getClassName()
+ */
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the View object associated with the {@link CanvasViewInfo}.
+ * @return the view object or null.
+ */
+ @Nullable
+ public Object getViewObject() {
+ return mViewObject;
+ }
+
+ /**
+ * Returns the baseline of this object, or -1 if it does not support a baseline
+ *
+ * @return the baseline or -1
+ */
+ public int getBaseline() {
+ if (mViewInfo != null) {
+ int baseline = mViewInfo.getBaseLine();
+ if (baseline != Integer.MIN_VALUE) {
+ return baseline;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns the {@link Margins} for this {@link CanvasViewInfo}
+ *
+ * @return the {@link Margins} for this {@link CanvasViewInfo}
+ */
+ @Nullable
+ public Margins getMargins() {
+ if (mViewInfo != null) {
+ int leftMargin = mViewInfo.getLeftMargin();
+ int topMargin = mViewInfo.getTopMargin();
+ int rightMargin = mViewInfo.getRightMargin();
+ int bottomMargin = mViewInfo.getBottomMargin();
+ return new Margins(
+ leftMargin != Integer.MIN_VALUE ? leftMargin : 0,
+ rightMargin != Integer.MIN_VALUE ? rightMargin : 0,
+ topMargin != Integer.MIN_VALUE ? topMargin : 0,
+ bottomMargin != Integer.MIN_VALUE ? bottomMargin : 0
+ );
+ }
+
+ return null;
+ }
+
+ // ---- Implementation of IPropertySource
+ // TODO: Get rid of this once the old propertysheet implementation is fully gone
+
+ @Override
+ public Object getEditableValue() {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ return ((IPropertySource) uiView).getEditableValue();
+ }
+ return null;
+ }
+
+ @Override
+ public IPropertyDescriptor[] getPropertyDescriptors() {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ return ((IPropertySource) uiView).getPropertyDescriptors();
+ }
+ return null;
+ }
+
+ @Override
+ public Object getPropertyValue(Object id) {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ return ((IPropertySource) uiView).getPropertyValue(id);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isPropertySet(Object id) {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ return ((IPropertySource) uiView).isPropertySet(id);
+ }
+ return false;
+ }
+
+ @Override
+ public void resetPropertyValue(Object id) {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ ((IPropertySource) uiView).resetPropertyValue(id);
+ }
+ }
+
+ @Override
+ public void setPropertyValue(Object id, Object value) {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ ((IPropertySource) uiView).setPropertyValue(id, value);
+ }
+ }
+
+ /**
+ * Returns the XML node corresponding to this info, or null if there is no
+ * such XML node.
+ *
+ * @return The XML node corresponding to this info object, or null
+ */
+ @Nullable
+ public Node getXmlNode() {
+ UiViewElementNode uiView = getUiViewNode();
+ if (uiView != null) {
+ return uiView.getXmlNode();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns true iff this view info corresponds to a root element.
+ *
+ * @return True iff this is a root view info.
+ */
+ public boolean isRoot() {
+ // Select the visual element -- unless it's the root.
+ // The root element is the one whose GRAND parent
+ // is null (because the parent will be a -document-
+ // node).
+
+ // Special case: a gesture overlay is sometimes added as the root, but for all intents
+ // and purposes it is its layout child that is the real root so treat that one as the
+ // root as well (such that the whole layout canvas does not highlight as part of hovers
+ // etc)
+ if (mParent != null
+ && mParent.mName.endsWith(GESTURE_OVERLAY_VIEW)
+ && mParent.isRoot()
+ && mParent.mChildren.size() == 1) {
+ return true;
+ }
+
+ return mUiViewNode == null || mUiViewNode.getUiParent() == null ||
+ mUiViewNode.getUiParent().getUiParent() == null;
+ }
+
+ /**
+ * Returns true if this {@link CanvasViewInfo} represents an invisible widget that
+ * should be highlighted when selected. This is the case for any layout that is less than the minimum
+ * threshold ({@link #SELECTION_MIN_SIZE}), or any other view that has -0- bounds.
+ *
+ * @return True if this is a tiny layout or invisible view
+ */
+ public boolean isInvisible() {
+ if (isHidden()) {
+ // Don't expand and highlight hidden widgets
+ return false;
+ }
+
+ if (mAbsRect.width < SELECTION_MIN_SIZE || mAbsRect.height < SELECTION_MIN_SIZE) {
+ return mUiViewNode != null && (mUiViewNode.getDescriptor().hasChildren() ||
+ mAbsRect.width <= 0 || mAbsRect.height <= 0);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if this {@link CanvasViewInfo} represents a widget that should be
+ * hidden, such as a {@code <Space>} which are typically not manipulated by the user
+ * through dragging etc.
+ *
+ * @return true if this is a hidden view
+ */
+ public boolean isHidden() {
+ if (GridLayoutRule.sDebugGridLayout) {
+ return false;
+ }
+
+ return FQCN_SPACE.equals(mName) || FQCN_SPACE_V7.equals(mName);
+ }
+
+ /**
+ * Is this {@link CanvasViewInfo} a view that has had its padding inflated in order to
+ * make it visible during selection or dragging? Note that this is NOT considered to
+ * be the case in the explode-all-views mode where all nodes have their padding
+ * increased; it's only used for views that individually exploded because they were
+ * requested visible and they returned true for {@link #isInvisible()}.
+ *
+ * @return True if this is an exploded node.
+ */
+ public boolean isExploded() {
+ return mExploded;
+ }
+
+ /**
+ * Mark this {@link CanvasViewInfo} as having been exploded or not. See the
+ * {@link #isExploded()} method for details on what this property means.
+ *
+ * @param exploded New value of the exploded property to mark this info with.
+ */
+ void setExploded(boolean exploded) {
+ mExploded = exploded;
+ }
+
+ /**
+ * Returns the info represented as a {@link SimpleElement}.
+ *
+ * @return A {@link SimpleElement} wrapping this info.
+ */
+ @NonNull
+ SimpleElement toSimpleElement() {
+
+ UiViewElementNode uiNode = getUiViewNode();
+
+ String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor());
+ String parentFqcn = null;
+ Rect bounds = SwtUtils.toRect(getAbsRect());
+ Rect parentBounds = null;
+
+ UiElementNode uiParent = uiNode.getUiParent();
+ if (uiParent != null) {
+ parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor());
+ }
+ if (getParent() != null) {
+ parentBounds = SwtUtils.toRect(getParent().getAbsRect());
+ }
+
+ SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds);
+
+ for (UiAttributeNode attr : uiNode.getAllUiAttributes()) {
+ String value = attr.getCurrentValue();
+ if (value != null && value.length() > 0) {
+ AttributeDescriptor attrDesc = attr.getDescriptor();
+ SimpleAttribute a = new SimpleAttribute(
+ attrDesc.getNamespaceUri(),
+ attrDesc.getXmlLocalName(),
+ value);
+ e.addAttribute(a);
+ }
+ }
+
+ for (CanvasViewInfo childVi : getChildren()) {
+ SimpleElement e2 = childVi.toSimpleElement();
+ if (e2 != null) {
+ e.addInnerElement(e2);
+ }
+ }
+
+ return e;
+ }
+
+ /**
+ * Returns the layout url attribute value for the closest surrounding include or
+ * fragment element parent, or null if this {@link CanvasViewInfo} is not rendered as
+ * part of an include or fragment tag.
+ *
+ * @return the layout url attribute value for the surrounding include tag, or null if
+ * not applicable
+ */
+ @Nullable
+ public String getIncludeUrl() {
+ CanvasViewInfo curr = this;
+ while (curr != null) {
+ if (curr.mUiViewNode != null) {
+ Node node = curr.mUiViewNode.getXmlNode();
+ if (node != null && node.getNodeType() == Node.ELEMENT_NODE) {
+ String nodeName = node.getNodeName();
+ if (node.getNamespaceURI() == null
+ && SdkConstants.VIEW_INCLUDE.equals(nodeName)) {
+ // Note: the layout attribute is NOT in the Android namespace
+ Element element = (Element) node;
+ String url = element.getAttribute(SdkConstants.ATTR_LAYOUT);
+ if (url.length() > 0) {
+ return url;
+ }
+ } else if (SdkConstants.VIEW_FRAGMENT.equals(nodeName)) {
+ String url = FragmentMenu.getFragmentLayout(node);
+ if (url != null) {
+ return url;
+ }
+ }
+ }
+ }
+ curr = curr.mParent;
+ }
+
+ return null;
+ }
+
+ /** Adds the given {@link CanvasViewInfo} as a new last child of this view */
+ private void addChild(@NonNull CanvasViewInfo child) {
+ mChildren.add(child);
+ }
+
+ /** Adds the given {@link CanvasViewInfo} as a child at the given index */
+ private void addChildAt(int index, @NonNull CanvasViewInfo child) {
+ mChildren.add(index, child);
+ }
+
+ /**
+ * Removes the given {@link CanvasViewInfo} from the child list of this view, and
+ * returns true if it was successfully removed
+ *
+ * @param child the child to be removed
+ * @return true if it was a child and was removed
+ */
+ public boolean removeChild(@NonNull CanvasViewInfo child) {
+ return mChildren.remove(child);
+ }
+
+ @Override
+ public String toString() {
+ return "CanvasViewInfo [name=" + mName + ", node=" + mUiViewNode + "]";
+ }
+
+ // ---- Factory functionality ----
+
+ /**
+ * Creates a new {@link CanvasViewInfo} hierarchy based on the given {@link ViewInfo}
+ * hierarchy. Note that this will not necessarily create one {@link CanvasViewInfo}
+ * for each {@link ViewInfo}. It will generally only create {@link CanvasViewInfo}
+ * objects for {@link ViewInfo} objects that contain a reference to an
+ * {@link UiViewElementNode}, meaning that it corresponds to an element in the XML
+ * file for this layout file. This is not always the case, such as in the following
+ * scenarios:
+ * <ul>
+ * <li>we link to other layouts with {@code <include>}
+ * <li>the current view is rendered within another view ("Show Included In") such that
+ * the outer file does not correspond to elements in the current included XML layout
+ * <li>on older platforms that don't support {@link Capability#EMBEDDED_LAYOUT} there
+ * is no reference to the {@code <include>} tag
+ * <li>with the {@code <merge>} tag we don't get a reference to the corresponding
+ * element
+ * <ul>
+ * <p>
+ * This method will build up a set of {@link CanvasViewInfo} that corresponds to the
+ * actual <b>selectable</b> views (which are also shown in the Outline).
+ *
+ * @param layoutlib5 if true, the {@link ViewInfo} hierarchy was created by layoutlib
+ * version 5 or higher, which means this algorithm can make certain assumptions
+ * (for example that {@code <merge>} siblings will provide {@link MergeCookie}
+ * references, so we don't have to search for them.)
+ * @param root the root {@link ViewInfo} to build from
+ * @return a {@link CanvasViewInfo} hierarchy
+ */
+ @NonNull
+ public static Pair<CanvasViewInfo,List<Rectangle>> create(ViewInfo root, boolean layoutlib5) {
+ return new Builder(layoutlib5).create(root);
+ }
+
+ /** Builder object which walks over a tree of {@link ViewInfo} objects and builds
+ * up a corresponding {@link CanvasViewInfo} hierarchy. */
+ private static class Builder {
+ public Builder(boolean layoutlib5) {
+ mLayoutLib5 = layoutlib5;
+ }
+
+ /**
+ * The mapping from nodes that have a {@code <merge>} as a parent in the node
+ * model to their corresponding views
+ */
+ private Map<UiViewElementNode, List<CanvasViewInfo>> mMergeNodeMap;
+
+ /**
+ * Whether the ViewInfos are provided by a layout library that is version 5 or
+ * later, since that will allow us to take several shortcuts
+ */
+ private boolean mLayoutLib5;
+
+ /**
+ * Creates a hierarchy of {@link CanvasViewInfo} objects and merge bounding
+ * rectangles from the given {@link ViewInfo} hierarchy
+ */
+ private Pair<CanvasViewInfo,List<Rectangle>> create(ViewInfo root) {
+ Object cookie = root.getCookie();
+ if (cookie == null) {
+ // Special case: If the root-most view does not have a view cookie,
+ // then we are rendering some outer layout surrounding this layout, and in
+ // that case we must search down the hierarchy for the (possibly multiple)
+ // sub-roots that correspond to elements in this layout, and place them inside
+ // an outer view that has no node. In the outline this item will be used to
+ // show the inclusion-context.
+ CanvasViewInfo rootView = createView(null, root, 0, 0);
+ addKeyedSubtrees(rootView, root, 0, 0);
+
+ List<Rectangle> includedBounds = new ArrayList<Rectangle>();
+ for (CanvasViewInfo vi : rootView.getChildren()) {
+ if (vi.getNodeSiblings() == null || vi.isPrimaryNodeSibling()) {
+ includedBounds.add(vi.getAbsRect());
+ }
+ }
+
+ // There are <merge> nodes here; see if we can insert it into the hierarchy
+ if (mMergeNodeMap != null) {
+ // Locate all the nodes that have a <merge> as a parent in the node model,
+ // and where the view sits at the top level inside the include-context node.
+ UiViewElementNode merge = null;
+ List<CanvasViewInfo> merged = new ArrayList<CanvasViewInfo>();
+ for (Map.Entry<UiViewElementNode, List<CanvasViewInfo>> entry : mMergeNodeMap
+ .entrySet()) {
+ UiViewElementNode node = entry.getKey();
+ if (!hasMergeParent(node)) {
+ continue;
+ }
+ List<CanvasViewInfo> views = entry.getValue();
+ assert views.size() > 0;
+ CanvasViewInfo view = views.get(0); // primary
+ if (view.getParent() != rootView) {
+ continue;
+ }
+ UiElementNode parent = node.getUiParent();
+ if (merge != null && parent != merge) {
+ continue;
+ }
+ merge = (UiViewElementNode) parent;
+ merged.add(view);
+ }
+ if (merged.size() > 0) {
+ // Compute a bounding box for the merged views
+ Rectangle absRect = null;
+ for (CanvasViewInfo child : merged) {
+ Rectangle rect = child.getAbsRect();
+ if (absRect == null) {
+ absRect = rect;
+ } else {
+ absRect = absRect.union(rect);
+ }
+ }
+
+ CanvasViewInfo mergeView = new CanvasViewInfo(rootView, VIEW_MERGE, null,
+ merge, absRect, absRect, null /* viewInfo */);
+ for (CanvasViewInfo view : merged) {
+ if (rootView.removeChild(view)) {
+ mergeView.addChild(view);
+ }
+ }
+ rootView.addChild(mergeView);
+ }
+ }
+
+ return Pair.of(rootView, includedBounds);
+ } else {
+ // We have a view key at the top, so just go and create {@link CanvasViewInfo}
+ // objects for each {@link ViewInfo} until we run into a null key.
+ CanvasViewInfo rootView = addKeyedSubtrees(null, root, 0, 0);
+
+ // Special case: look to see if the root element is really a <merge>, and if so,
+ // manufacture a view for it such that we can target this root element
+ // in drag & drop operations, such that we can show it in the outline, etc
+ if (rootView != null && hasMergeParent(rootView.getUiViewNode())) {
+ CanvasViewInfo merge = new CanvasViewInfo(null, VIEW_MERGE, null,
+ (UiViewElementNode) rootView.getUiViewNode().getUiParent(),
+ rootView.getAbsRect(), rootView.getSelectionRect(),
+ null /* viewInfo */);
+ // Insert the <merge> as the new real root
+ rootView.mParent = merge;
+ merge.addChild(rootView);
+ rootView = merge;
+ }
+
+ return Pair.of(rootView, null);
+ }
+ }
+
+ private boolean hasMergeParent(UiViewElementNode rootNode) {
+ UiElementNode rootParent = rootNode.getUiParent();
+ return (rootParent instanceof UiViewElementNode
+ && VIEW_MERGE.equals(rootParent.getDescriptor().getXmlName()));
+ }
+
+ /** Creates a {@link CanvasViewInfo} for a given {@link ViewInfo} but does not recurse */
+ private CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX,
+ int parentY) {
+ Object cookie = root.getCookie();
+ UiViewElementNode node = null;
+ if (cookie instanceof UiViewElementNode) {
+ node = (UiViewElementNode) cookie;
+ } else if (cookie instanceof MergeCookie) {
+ cookie = ((MergeCookie) cookie).getCookie();
+ if (cookie instanceof UiViewElementNode) {
+ node = (UiViewElementNode) cookie;
+ CanvasViewInfo view = createView(parent, root, parentX, parentY, node);
+ if (root.getCookie() instanceof MergeCookie && view.mNodeSiblings == null) {
+ List<CanvasViewInfo> v = mMergeNodeMap == null ?
+ null : mMergeNodeMap.get(node);
+ if (v != null) {
+ v.add(view);
+ } else {
+ v = new ArrayList<CanvasViewInfo>();
+ v.add(view);
+ if (mMergeNodeMap == null) {
+ mMergeNodeMap =
+ new HashMap<UiViewElementNode, List<CanvasViewInfo>>();
+ }
+ mMergeNodeMap.put(node, v);
+ }
+ view.mNodeSiblings = v;
+ }
+
+ return view;
+ }
+ }
+
+ return createView(parent, root, parentX, parentY, node);
+ }
+
+ /**
+ * Creates a {@link CanvasViewInfo} for a given {@link ViewInfo} but does not recurse.
+ * This method specifies an explicit {@link UiViewElementNode} to use rather than
+ * relying on the view cookie in the info object.
+ */
+ private CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX,
+ int parentY, UiViewElementNode node) {
+
+ int x = root.getLeft();
+ int y = root.getTop();
+ int w = root.getRight() - x;
+ int h = root.getBottom() - y;
+
+ x += parentX;
+ y += parentY;
+
+ Rectangle absRect = new Rectangle(x, y, w - 1, h - 1);
+
+ if (w < SELECTION_MIN_SIZE) {
+ int d = (SELECTION_MIN_SIZE - w) / 2;
+ x -= d;
+ w += SELECTION_MIN_SIZE - w;
+ }
+
+ if (h < SELECTION_MIN_SIZE) {
+ int d = (SELECTION_MIN_SIZE - h) / 2;
+ y -= d;
+ h += SELECTION_MIN_SIZE - h;
+ }
+
+ Rectangle selectionRect = new Rectangle(x, y, w - 1, h - 1);
+
+ return new CanvasViewInfo(parent, root.getClassName(), root.getViewObject(), node,
+ absRect, selectionRect, root);
+ }
+
+ /** Create a subtree recursively until you run out of keys */
+ private CanvasViewInfo createSubtree(CanvasViewInfo parent, ViewInfo viewInfo,
+ int parentX, int parentY) {
+ assert viewInfo.getCookie() != null;
+
+ CanvasViewInfo view = createView(parent, viewInfo, parentX, parentY);
+ // Bug workaround: Ensure that we never have a child node identical
+ // to its parent node: this can happen for example when rendering a
+ // ZoomControls view where the merge cookies point to the parent.
+ if (parent != null && view.mUiViewNode == parent.mUiViewNode) {
+ return null;
+ }
+
+ // Process children:
+ parentX += viewInfo.getLeft();
+ parentY += viewInfo.getTop();
+
+ List<ViewInfo> children = viewInfo.getChildren();
+
+ if (mLayoutLib5) {
+ for (ViewInfo child : children) {
+ Object cookie = child.getCookie();
+ if (cookie instanceof UiViewElementNode || cookie instanceof MergeCookie) {
+ CanvasViewInfo childView = createSubtree(view, child,
+ parentX, parentY);
+ if (childView != null) {
+ view.addChild(childView);
+ }
+ } // else: null cookies, adapter item references, etc: No child views.
+ }
+
+ return view;
+ }
+
+ // See if we have any missing keys at this level
+ int missingNodes = 0;
+ int mergeNodes = 0;
+ for (ViewInfo child : children) {
+ // Only use children which have a ViewKey of the correct type.
+ // We can't interact with those when they have a null key or
+ // an incompatible type.
+ Object cookie = child.getCookie();
+ if (!(cookie instanceof UiViewElementNode)) {
+ if (cookie instanceof MergeCookie) {
+ mergeNodes++;
+ } else {
+ missingNodes++;
+ }
+ }
+ }
+
+ if (missingNodes == 0 && mergeNodes == 0) {
+ // No missing nodes; this is the normal case, and we can just continue to
+ // recursively add our children
+ for (ViewInfo child : children) {
+ CanvasViewInfo childView = createSubtree(view, child,
+ parentX, parentY);
+ view.addChild(childView);
+ }
+
+ // TBD: Emit placeholder views for keys that have no views?
+ } else {
+ // We don't have keys for one or more of the ViewInfos. There are many
+ // possible causes: we are on an SDK platform that does not support
+ // embedded_layout rendering, or we are including a view with a <merge>
+ // as the root element.
+
+ UiViewElementNode uiViewNode = view.getUiViewNode();
+ String containerName = uiViewNode != null
+ ? uiViewNode.getDescriptor().getXmlLocalName() : ""; //$NON-NLS-1$
+ if (containerName.equals(SdkConstants.VIEW_INCLUDE)) {
+ // This is expected -- we don't WANT to get node keys for the content
+ // of an include since it's in a different file and should be treated
+ // as a single unit that cannot be edited (hence, no CanvasViewInfo
+ // children)
+ } else {
+ // We are getting children with null keys where we don't expect it;
+ // this usually means that we are dealing with an Android platform
+ // that does not support {@link Capability#EMBEDDED_LAYOUT}, or
+ // that there are <merge> tags which are doing surprising things
+ // to the view hierarchy
+ LinkedList<UiViewElementNode> unused = new LinkedList<UiViewElementNode>();
+ if (uiViewNode != null) {
+ for (UiElementNode child : uiViewNode.getUiChildren()) {
+ if (child instanceof UiViewElementNode) {
+ unused.addLast((UiViewElementNode) child);
+ }
+ }
+ }
+ for (ViewInfo child : children) {
+ Object cookie = child.getCookie();
+ if (mergeNodes > 0 && cookie instanceof MergeCookie) {
+ cookie = ((MergeCookie) cookie).getCookie();
+ }
+ if (cookie != null) {
+ unused.remove(cookie);
+ }
+ }
+
+ if (unused.size() > 0 || mergeNodes > 0) {
+ if (unused.size() == missingNodes) {
+ // The number of unmatched elements and ViewInfos are identical;
+ // it's very likely that they match one to one, so just use these
+ for (ViewInfo child : children) {
+ if (child.getCookie() == null) {
+ // Only create a flat (non-recursive) view
+ CanvasViewInfo childView = createView(view, child, parentX,
+ parentY, unused.removeFirst());
+ view.addChild(childView);
+ } else {
+ CanvasViewInfo childView = createSubtree(view, child, parentX,
+ parentY);
+ view.addChild(childView);
+ }
+ }
+ } else {
+ // We have an uneven match. In this case we might be dealing
+ // with <merge> etc.
+ // We have no way to associate elements back with the
+ // corresponding <include> tags if there are more than one of
+ // them. That's not a huge tragedy since visually you are not
+ // allowed to edit these anyway; we just need to make a visual
+ // block for these for selection and outline purposes.
+ addMismatched(view, parentX, parentY, children, unused);
+ }
+ } else {
+ // No unused keys, but there are views without keys.
+ // We can't represent these since all views must have node keys
+ // such that you can operate on them. Just ignore these.
+ for (ViewInfo child : children) {
+ if (child.getCookie() != null) {
+ CanvasViewInfo childView = createSubtree(view, child,
+ parentX, parentY);
+ view.addChild(childView);
+ }
+ }
+ }
+ }
+ }
+
+ return view;
+ }
+
+ /**
+ * We have various {@link ViewInfo} children with null keys, and/or nodes in
+ * the corresponding UI model that are not referenced by any of the {@link ViewInfo}
+ * objects. This method attempts to account for this, by matching the views in
+ * the right order.
+ */
+ private void addMismatched(CanvasViewInfo parentView, int parentX, int parentY,
+ List<ViewInfo> children, LinkedList<UiViewElementNode> unused) {
+ UiViewElementNode afterNode = null;
+ UiViewElementNode beforeNode = null;
+ // We have one important clue we can use when matching unused nodes
+ // with views: if we have a view V1 with node N1, and a view V2 with node N2,
+ // then we can only match unknown node UN with unknown node UV if
+ // V1 < UV < V2 and N1 < UN < N2.
+ // We can use these constraints to do the matching, for example by
+ // a simple DAG traversal. However, since the number of unmatched nodes
+ // will typically be very small, we'll just do a simple algorithm here
+ // which checks forwards/backwards whether a match is valid.
+ for (int index = 0, size = children.size(); index < size; index++) {
+ ViewInfo child = children.get(index);
+ if (child.getCookie() != null) {
+ CanvasViewInfo childView = createSubtree(parentView, child, parentX, parentY);
+ if (childView != null) {
+ parentView.addChild(childView);
+ }
+ if (child.getCookie() instanceof UiViewElementNode) {
+ afterNode = (UiViewElementNode) child.getCookie();
+ }
+ } else {
+ beforeNode = nextViewNode(children, index);
+
+ // Find first eligible node from unused
+ // TOD: What if there are more eligible? We need to process ALL views
+ // and all nodes in one go here
+
+ UiViewElementNode matching = null;
+ for (UiViewElementNode candidate : unused) {
+ if (afterNode == null || isAfter(afterNode, candidate)) {
+ if (beforeNode == null || isBefore(beforeNode, candidate)) {
+ matching = candidate;
+ break;
+ }
+ }
+ }
+
+ if (matching != null) {
+ unused.remove(matching);
+ CanvasViewInfo childView = createView(parentView, child, parentX, parentY,
+ matching);
+ parentView.addChild(childView);
+ afterNode = matching;
+ } else {
+ // We have no node for the view -- what do we do??
+ // Nothing - we only represent stuff in the outline that is in the
+ // source model, not in the render
+ }
+ }
+ }
+
+ // Add zero-bounded boxes for all remaining nodes since they need to show
+ // up in the outline, need to be selectable so you can press Delete, etc.
+ if (unused.size() > 0) {
+ Map<UiViewElementNode, Integer> rankMap =
+ new HashMap<UiViewElementNode, Integer>();
+ Map<UiViewElementNode, CanvasViewInfo> infoMap =
+ new HashMap<UiViewElementNode, CanvasViewInfo>();
+ UiElementNode parent = unused.get(0).getUiParent();
+ if (parent != null) {
+ int index = 0;
+ for (UiElementNode child : parent.getUiChildren()) {
+ UiViewElementNode node = (UiViewElementNode) child;
+ rankMap.put(node, index++);
+ }
+ for (CanvasViewInfo child : parentView.getChildren()) {
+ infoMap.put(child.getUiViewNode(), child);
+ }
+ List<Integer> usedIndexes = new ArrayList<Integer>();
+ for (UiViewElementNode node : unused) {
+ Integer rank = rankMap.get(node);
+ if (rank != null) {
+ usedIndexes.add(rank);
+ }
+ }
+ Collections.sort(usedIndexes);
+ for (int i = usedIndexes.size() - 1; i >= 0; i--) {
+ Integer rank = usedIndexes.get(i);
+ UiViewElementNode found = null;
+ for (UiViewElementNode node : unused) {
+ if (rankMap.get(node) == rank) {
+ found = node;
+ break;
+ }
+ }
+ if (found != null) {
+ Rectangle absRect = new Rectangle(parentX, parentY, 0, 0);
+ String name = found.getDescriptor().getXmlLocalName();
+ CanvasViewInfo v = new CanvasViewInfo(parentView, name, null, found,
+ absRect, absRect, null /* viewInfo */);
+ // Find corresponding index in the parent view
+ List<CanvasViewInfo> siblings = parentView.getChildren();
+ int insertPosition = siblings.size();
+ for (int j = siblings.size() - 1; j >= 0; j--) {
+ CanvasViewInfo sibling = siblings.get(j);
+ UiViewElementNode siblingNode = sibling.getUiViewNode();
+ if (siblingNode != null) {
+ Integer siblingRank = rankMap.get(siblingNode);
+ if (siblingRank != null && siblingRank < rank) {
+ insertPosition = j + 1;
+ break;
+ }
+ }
+ }
+ parentView.addChildAt(insertPosition, v);
+ unused.remove(found);
+ }
+ }
+ }
+ // Add in any remaining
+ for (UiViewElementNode node : unused) {
+ Rectangle absRect = new Rectangle(parentX, parentY, 0, 0);
+ String name = node.getDescriptor().getXmlLocalName();
+ CanvasViewInfo v = new CanvasViewInfo(parentView, name, null, node, absRect,
+ absRect, null /* viewInfo */);
+ parentView.addChild(v);
+ }
+ }
+ }
+
+ private boolean isBefore(UiViewElementNode beforeNode, UiViewElementNode candidate) {
+ UiElementNode parent = candidate.getUiParent();
+ if (parent != null) {
+ for (UiElementNode sibling : parent.getUiChildren()) {
+ if (sibling == beforeNode) {
+ return false;
+ } else if (sibling == candidate) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean isAfter(UiViewElementNode afterNode, UiViewElementNode candidate) {
+ UiElementNode parent = candidate.getUiParent();
+ if (parent != null) {
+ for (UiElementNode sibling : parent.getUiChildren()) {
+ if (sibling == afterNode) {
+ return true;
+ } else if (sibling == candidate) {
+ return false;
+ }
+ }
+ }
+ return false;
+ }
+
+ private UiViewElementNode nextViewNode(List<ViewInfo> children, int index) {
+ int size = children.size();
+ for (; index < size; index++) {
+ ViewInfo child = children.get(index);
+ if (child.getCookie() instanceof UiViewElementNode) {
+ return (UiViewElementNode) child.getCookie();
+ }
+ }
+
+ return null;
+ }
+
+ /** Search for a subtree with valid keys and add those subtrees */
+ private CanvasViewInfo addKeyedSubtrees(CanvasViewInfo parent, ViewInfo viewInfo,
+ int parentX, int parentY) {
+ // We don't include MergeCookies when searching down for the first non-null key,
+ // since this means we are in a "Show Included In" context, and the include tag itself
+ // (which the merge cookie is pointing to) is still in the including-document rather
+ // than the included document. Therefore, we only accept real UiViewElementNodes here,
+ // not MergeCookies.
+ if (viewInfo.getCookie() != null) {
+ CanvasViewInfo subtree = createSubtree(parent, viewInfo, parentX, parentY);
+ if (parent != null && subtree != null) {
+ parent.mChildren.add(subtree);
+ }
+ return subtree;
+ } else {
+ for (ViewInfo child : viewInfo.getChildren()) {
+ addKeyedSubtrees(parent, child, parentX + viewInfo.getLeft(), parentY
+ + viewInfo.getTop());
+ }
+
+ return null;
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java
new file mode 100644
index 000000000..263456984
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2010 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 static com.android.SdkConstants.ANDROID_NS_NAME;
+import static com.android.SdkConstants.NS_RESOURCES;
+import static com.android.SdkConstants.XMLNS_URI;
+
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.IDragElement.IDragAttribute;
+import com.android.ide.common.api.INode;
+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.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+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 org.eclipse.jface.action.Action;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.widgets.Composite;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The {@link ClipboardSupport} class manages the native clipboard, providing operations
+ * to copy, cut and paste view items, and can answer whether the clipboard contains
+ * a transferable we care about.
+ */
+public class ClipboardSupport {
+ private static final boolean DEBUG = false;
+
+ /** SWT clipboard instance. */
+ private Clipboard mClipboard;
+ private LayoutCanvas mCanvas;
+
+ /**
+ * Constructs a new {@link ClipboardSupport} tied to the given
+ * {@link LayoutCanvas}.
+ *
+ * @param canvas The {@link LayoutCanvas} to provide clipboard support for.
+ * @param parent The parent widget in the SWT hierarchy of the canvas.
+ */
+ public ClipboardSupport(LayoutCanvas canvas, Composite parent) {
+ mCanvas = canvas;
+
+ mClipboard = new Clipboard(parent.getDisplay());
+ }
+
+ /**
+ * Frees up any resources held by the {@link ClipboardSupport}.
+ */
+ public void dispose() {
+ if (mClipboard != null) {
+ mClipboard.dispose();
+ mClipboard = null;
+ }
+ }
+
+ /**
+ * Perform the "Copy" action, either from the Edit menu or from the context
+ * menu.
+ * <p/>
+ * This sanitizes the selection, so it must be a copy. It then inserts the
+ * selection both as text and as {@link SimpleElement}s in the clipboard.
+ * (If there is selected text in the error label, then the error is used
+ * as the text portion of the transferable.)
+ *
+ * @param selection A list of selection items to add to the clipboard;
+ * <b>this should be a copy already - this method will not make a
+ * copy</b>
+ */
+ public void copySelectionToClipboard(List<SelectionItem> selection) {
+ SelectionManager.sanitize(selection);
+
+ // The error message area shares the copy action with the canvas. Invoking the
+ // copy action when there are errors visible *AND* the user has selected text there,
+ // should include the error message as the text transferable.
+ String message = null;
+ GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ StyledText errorLabel = graphicalEditor.getErrorLabel();
+ if (errorLabel.getSelectionCount() > 0) {
+ message = errorLabel.getSelectionText();
+ }
+
+ if (selection.isEmpty()) {
+ if (message != null) {
+ mClipboard.setContents(
+ new Object[] { message },
+ new Transfer[] { TextTransfer.getInstance() }
+ );
+ }
+ return;
+ }
+
+ Object[] data = new Object[] {
+ SelectionItem.getAsElements(selection),
+ message != null ? message : SelectionItem.getAsText(mCanvas, selection)
+ };
+
+ Transfer[] types = new Transfer[] {
+ SimpleXmlTransfer.getInstance(),
+ TextTransfer.getInstance()
+ };
+
+ mClipboard.setContents(data, types);
+ }
+
+ /**
+ * Perform the "Cut" action, either from the Edit menu or from the context
+ * menu.
+ * <p/>
+ * This sanitizes the selection, so it must be a copy. It uses the
+ * {@link #copySelectionToClipboard(List)} method to copy the selection to
+ * the clipboard. Finally it uses {@link #deleteSelection(String, List)} to
+ * delete the selection with a "Cut" verb for the title.
+ *
+ * @param selection A list of selection items to add to the clipboard;
+ * <b>this should be a copy already - this method will not make a
+ * copy</b>
+ */
+ public void cutSelectionToClipboard(List<SelectionItem> selection) {
+ copySelectionToClipboard(selection);
+ deleteSelection(mCanvas.getCutLabel(), selection);
+ }
+
+ /**
+ * Deletes the given selection.
+ *
+ * @param verb A translated verb for the action. Will be used for the
+ * undo/redo title. Typically this should be
+ * {@link Action#getText()} for either the cut or the delete
+ * actions in the canvas.
+ * @param selection The selection. Must not be null. Can be empty, in which
+ * case nothing happens. The selection list will be sanitized so
+ * the caller should pass in a copy.
+ */
+ public void deleteSelection(String verb, final List<SelectionItem> selection) {
+ SelectionManager.sanitize(selection);
+
+ if (selection.isEmpty()) {
+ return;
+ }
+
+ // If all selected items have the same *kind* of parent, display that in the undo title.
+ String title = null;
+ for (SelectionItem cs : selection) {
+ CanvasViewInfo vi = cs.getViewInfo();
+ if (vi != null && vi.getParent() != null) {
+ CanvasViewInfo parent = vi.getParent();
+ assert parent != null;
+ if (title == null) {
+ title = parent.getName();
+ } else if (!title.equals(parent.getName())) {
+ // More than one kind of parent selected.
+ title = null;
+ break;
+ }
+ }
+ }
+
+ if (title != null) {
+ // Typically the name is an FQCN. Just get the last segment.
+ int pos = title.lastIndexOf('.');
+ if (pos > 0 && pos < title.length() - 1) {
+ title = title.substring(pos + 1);
+ }
+ }
+ boolean multiple = mCanvas.getSelectionManager().hasMultiSelection();
+ if (title == null) {
+ title = String.format(
+ multiple ? "%1$s elements" : "%1$s element",
+ verb);
+ } else {
+ title = String.format(
+ multiple ? "%1$s elements from %2$s" : "%1$s element from %2$s",
+ verb, title);
+ }
+
+ // Implementation note: we don't clear the internal selection after removing
+ // the elements. An update XML model event should happen when the model gets released
+ // which will trigger a recompute of the layout, thus reloading the model thus
+ // resetting the selection.
+ mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(title, new Runnable() {
+ @Override
+ public void run() {
+ // Segment the deleted nodes into clusters of siblings
+ Map<NodeProxy, List<INode>> clusters =
+ new HashMap<NodeProxy, List<INode>>();
+ for (SelectionItem cs : selection) {
+ NodeProxy node = cs.getNode();
+ if (node == null) {
+ continue;
+ }
+ INode parent = node.getParent();
+ if (parent != null) {
+ List<INode> children = clusters.get(parent);
+ if (children == null) {
+ children = new ArrayList<INode>();
+ clusters.put((NodeProxy) parent, children);
+ }
+ children.add(node);
+ }
+ }
+
+ // Notify parent views about children getting deleted
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ for (Map.Entry<NodeProxy, List<INode>> entry : clusters.entrySet()) {
+ NodeProxy parent = entry.getKey();
+ List<INode> children = entry.getValue();
+ assert children != null && children.size() > 0;
+ rulesEngine.callOnRemovingChildren(parent, children);
+ parent.applyPendingChanges();
+ }
+
+ for (SelectionItem cs : selection) {
+ CanvasViewInfo vi = cs.getViewInfo();
+ // You can't delete the root element
+ if (vi != null && !vi.isRoot()) {
+ UiViewElementNode ui = vi.getUiViewNode();
+ if (ui != null) {
+ ui.deleteXmlNode();
+ }
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Perform the "Paste" action, either from the Edit menu or from the context
+ * menu.
+ *
+ * @param selection A list of selection items to add to the clipboard;
+ * <b>this should be a copy already - this method will not make a
+ * copy</b>
+ */
+ public void pasteSelection(List<SelectionItem> selection) {
+
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+ final SimpleElement[] pasted = (SimpleElement[]) mClipboard.getContents(sxt);
+
+ if (pasted == null || pasted.length == 0) {
+ return;
+ }
+
+ CanvasViewInfo lastRoot = mCanvas.getViewHierarchy().getRoot();
+ if (lastRoot == null) {
+ // Pasting in an empty document. Only paste the first element.
+ pasteInEmptyDocument(pasted[0]);
+ return;
+ }
+
+ // Otherwise use the current selection, if any, as a guide where to paste
+ // using the first selected element only. If there's no selection use
+ // the root as the insertion point.
+ SelectionManager.sanitize(selection);
+ final CanvasViewInfo target;
+ if (selection.size() > 0) {
+ SelectionItem cs = selection.get(0);
+ target = cs.getViewInfo();
+ } else {
+ target = lastRoot;
+ }
+
+ final NodeProxy targetNode = mCanvas.getNodeFactory().create(target);
+ mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel("Paste", new Runnable() {
+ @Override
+ public void run() {
+ RulesEngine engine = mCanvas.getRulesEngine();
+ NodeProxy node = engine.callOnPaste(targetNode, target.getViewObject(), pasted);
+ node.applyPendingChanges();
+ }
+ });
+ }
+
+ /**
+ * Paste a new root into an 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/>
+ * Implementation is similar to {@link #createDocumentRoot} except we also
+ * copy all the attributes and inner elements recursively.
+ */
+ private void pasteInEmptyDocument(final IDragElement pastedElement) {
+ String rootFqcn = pastedElement.getFqcn();
+
+ // Need a valid empty document to create the new root
+ final LayoutEditorDelegate delegate = mCanvas.getEditorDelegate();
+ final UiDocumentNode uiDoc = delegate.getUiRootNode();
+ if (uiDoc == null || uiDoc.getUiChildren().size() > 0) {
+ debugPrintf("Failed to paste document root for %1$s: document is not empty", rootFqcn);
+ return;
+ }
+
+ // Find the view descriptor matching our FQCN
+ final ViewElementDescriptor viewDesc = delegate.getFqcnViewDescriptor(rootFqcn);
+ if (viewDesc == null) {
+ // TODO this could happen if pasting a custom view not known in this project
+ debugPrintf("Failed to paste 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("Paste root %1$s in document", title);
+
+ delegate.getEditor().wrapUndoEditXmlModel(title, new Runnable() {
+ @Override
+ public void run() {
+ UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc);
+
+ // A root node requires the Android XMLNS
+ uiNew.setAttributeValue(ANDROID_NS_NAME, XMLNS_URI, NS_RESOURCES,
+ true /*override*/);
+
+ // Copy all the attributes from the pasted element
+ for (IDragAttribute attr : pastedElement.getAttributes()) {
+ uiNew.setAttributeValue(
+ attr.getName(),
+ attr.getUri(),
+ attr.getValue(),
+ true /*override*/);
+ }
+
+ // Adjust the attributes, adding the default layout_width/height
+ // only if they are not present (the original element should have
+ // them though.)
+ DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
+
+ uiNew.createXmlNode();
+
+ // Now process all children
+ for (IDragElement childElement : pastedElement.getInnerElements()) {
+ addChild(uiNew, childElement);
+ }
+ }
+
+ private void addChild(UiElementNode uiParent, IDragElement childElement) {
+ String childFqcn = childElement.getFqcn();
+ final ViewElementDescriptor childDesc =
+ delegate.getFqcnViewDescriptor(childFqcn);
+ if (childDesc == null) {
+ // TODO this could happen if pasting a custom view
+ debugPrintf("Failed to paste element, unknown FQCN %1$s", childFqcn);
+ return;
+ }
+
+ UiElementNode uiChild = uiParent.appendNewUiChild(childDesc);
+
+ // Copy all the attributes from the pasted element
+ for (IDragAttribute attr : childElement.getAttributes()) {
+ uiChild.setAttributeValue(
+ attr.getName(),
+ attr.getUri(),
+ attr.getValue(),
+ true /*override*/);
+ }
+
+ // Adjust the attributes, adding the default layout_width/height
+ // only if they are not present (the original element should have
+ // them though.)
+ DescriptorsUtils.setDefaultLayoutAttributes(
+ uiChild, false /*updateLayout*/);
+
+ uiChild.createXmlNode();
+
+ // Now process all grand children
+ for (IDragElement grandChildElement : childElement.getInnerElements()) {
+ addChild(uiChild, grandChildElement);
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns true if we have a a simple xml transfer data object on the
+ * clipboard.
+ *
+ * @return True if and only if the clipboard contains one of XML element
+ * objects.
+ */
+ public boolean hasSxtOnClipboard() {
+ // 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.
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+ for (TransferData td : mClipboard.getAvailableTypes()) {
+ if (sxt.isSupportedType(td)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void debugPrintf(String message, Object... params) {
+ if (DEBUG) AdtPlugin.printToConsole("Clipboard", String.format(message, params));
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java
new file mode 100644
index 000000000..55930f6cd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2010 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 org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.events.MenuDetectEvent;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.graphics.Point;
+
+/**
+ * A {@link ControlPoint} is a coordinate in the canvas control which corresponds
+ * exactly to (0,0) at the top left of the canvas. It is unaffected by canvas
+ * zooming.
+ */
+public final class ControlPoint {
+ /** Containing canvas which the point is relative to. */
+ private final LayoutCanvas mCanvas;
+
+ /** The X coordinate of the mouse coordinate. */
+ public final int x;
+
+ /** The Y coordinate of the mouse coordinate. */
+ public final int y;
+
+ /**
+ * Constructs a new {@link ControlPoint} from the given event. The event
+ * must be from a {@link MouseListener} associated with the
+ * {@link LayoutCanvas} such that the {@link MouseEvent#x} and
+ * {@link MouseEvent#y} fields are relative to the canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The mouse event to construct the {@link ControlPoint}
+ * from.
+ * @return A {@link ControlPoint} which corresponds to the given
+ * {@link MouseEvent}.
+ */
+ public static ControlPoint create(LayoutCanvas canvas, MouseEvent event) {
+ // The mouse event coordinates should already be relative to the canvas
+ // widget.
+ assert event.widget == canvas : event.widget;
+ return new ControlPoint(canvas, event.x, event.y);
+ }
+
+ /**
+ * Constructs a new {@link ControlPoint} from the given menu detect event.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The menu detect event to construct the {@link ControlPoint} from.
+ * @return A {@link ControlPoint} which corresponds to the given
+ * {@link MenuDetectEvent}.
+ */
+ public static ControlPoint create(LayoutCanvas canvas, MenuDetectEvent event) {
+ // The menu detect events are always display-relative.
+ org.eclipse.swt.graphics.Point p = canvas.toControl(event.x, event.y);
+ return new ControlPoint(canvas, p.x, p.y);
+ }
+
+ /**
+ * Constructs a new {@link ControlPoint} from the given event. The event
+ * must be from a {@link DragSourceListener} associated with the
+ * {@link LayoutCanvas} such that the {@link DragSourceEvent#x} and
+ * {@link DragSourceEvent#y} fields are relative to the canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The mouse event to construct the {@link ControlPoint}
+ * from.
+ * @return A {@link ControlPoint} which corresponds to the given
+ * {@link DragSourceEvent}.
+ */
+ public static ControlPoint create(LayoutCanvas canvas, DragSourceEvent event) {
+ // The drag source event coordinates should already be relative to the
+ // canvas widget.
+ return new ControlPoint(canvas, event.x, event.y);
+ }
+
+ /**
+ * Constructs a new {@link ControlPoint} from the given event.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The mouse event to construct the {@link ControlPoint}
+ * from.
+ * @return A {@link ControlPoint} which corresponds to the given
+ * {@link DropTargetEvent}.
+ */
+ public static ControlPoint create(LayoutCanvas canvas, DropTargetEvent event) {
+ // The drop target events are always relative to the display, so we must
+ // first convert them to be canvas relative.
+ org.eclipse.swt.graphics.Point p = canvas.toControl(event.x, event.y);
+ return new ControlPoint(canvas, p.x, p.y);
+ }
+
+ /**
+ * Constructs a new {@link ControlPoint} from the given x,y coordinates,
+ * which must be relative to the given {@link LayoutCanvas}.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param x The mouse event x coordinate relative to the canvas
+ * @param y The mouse event x coordinate relative to the canvas
+ * @return A {@link ControlPoint} which corresponds to the given
+ * coordinates.
+ */
+ public static ControlPoint create(LayoutCanvas canvas, int x, int y) {
+ return new ControlPoint(canvas, x, y);
+ }
+
+ /**
+ * Constructs a new canvas control coordinate with the given X and Y
+ * coordinates. This is private; use one of the factory methods
+ * {@link #create(LayoutCanvas, MouseEvent)},
+ * {@link #create(LayoutCanvas, DragSourceEvent)} or
+ * {@link #create(LayoutCanvas, DropTargetEvent)} instead.
+ *
+ * @param canvas The canvas which contains this coordinate
+ * @param x The mouse x coordinate
+ * @param y The mouse y coordinate
+ */
+ private ControlPoint(LayoutCanvas canvas, int x, int y) {
+ mCanvas = canvas;
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * Returns the equivalent {@link LayoutPoint} to this
+ * {@link ControlPoint}.
+ *
+ * @return The equivalent {@link LayoutPoint} to this
+ * {@link ControlPoint}.
+ */
+ public LayoutPoint toLayout() {
+ int lx = mCanvas.getHorizontalTransform().inverseTranslate(x);
+ int ly = mCanvas.getVerticalTransform().inverseTranslate(y);
+
+ return LayoutPoint.create(mCanvas, lx, ly);
+ }
+
+ @Override
+ public String toString() {
+ return "ControlPoint [x=" + x + ", y=" + y + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + x;
+ result = prime * result + y;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ ControlPoint other = (ControlPoint) obj;
+ if (x != other.x)
+ return false;
+ if (y != other.y)
+ return false;
+ if (mCanvas != other.mCanvas) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns this point as an SWT point in the display coordinate system
+ *
+ * @return this point as an SWT point in the display coordinate system
+ */
+ public Point toDisplayPoint() {
+ return mCanvas.toDisplay(x, y);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CreateNewConfigJob.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CreateNewConfigJob.java
new file mode 100644
index 000000000..44cd0810f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CreateNewConfigJob.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2012 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.annotations.NonNull;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.resources.ResourceFolderType;
+import com.google.common.base.Charsets;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.PartInitException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+/** Job which creates a new layout file for a given configuration */
+class CreateNewConfigJob extends Job {
+ private final GraphicalEditorPart mEditor;
+ private final IFile mFromFile;
+ private final FolderConfiguration mConfig;
+
+ CreateNewConfigJob(
+ @NonNull GraphicalEditorPart editor,
+ @NonNull IFile fromFile,
+ @NonNull FolderConfiguration config) {
+ super("Create Alternate Layout");
+ mEditor = editor;
+ mFromFile = fromFile;
+ mConfig = config;
+ }
+
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ // get the folder name
+ String folderName = mConfig.getFolderName(ResourceFolderType.LAYOUT);
+ try {
+ // look to see if it exists.
+ // get the res folder
+ IFolder res = (IFolder) mFromFile.getParent().getParent();
+
+ IFolder newParentFolder = res.getFolder(folderName);
+ AdtUtils.ensureExists(newParentFolder);
+ final IFile file = newParentFolder.getFile(mFromFile.getName());
+ if (file.exists()) {
+ String message = String.format("File 'res/%1$s/%2$s' already exists!",
+ folderName, mFromFile.getName());
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message);
+ }
+
+ // Read current document contents instead of from file: mFromFile.getContents()
+ String text = mEditor.getEditorDelegate().getEditor().getStructuredDocument().get();
+ ByteArrayInputStream input = new ByteArrayInputStream(text.getBytes(Charsets.UTF_8));
+ file.create(input, false, monitor);
+ input.close();
+
+ // Ensure that the project resources updates itself to notice the new configuration.
+ // In theory, this shouldn't be necessary, but we need to make sure the
+ // resource manager knows about this immediately such that the call below
+ // to find the best configuration takes the new folder into account.
+ ResourceManager resourceManager = ResourceManager.getInstance();
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ IFolder folder = root.getFolder(newParentFolder.getFullPath());
+ resourceManager.getResourceFolder(folder);
+
+ // Switch to the new file
+ Display display = mEditor.getConfigurationChooser().getDisplay();
+ display.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ // The given old layout has been forked into a new layout
+ // for a given configuration. This means that the old layout
+ // is no longer a match for the configuration, which is
+ // probably what it is still showing. We have to modify
+ // its configuration to no longer be an impossible
+ // configuration.
+ ConfigurationChooser chooser = mEditor.getConfigurationChooser();
+ chooser.onAlternateLayoutCreated();
+
+ // Finally open the new layout
+ try {
+ AdtPlugin.openFile(file, null, false);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ });
+ } catch (IOException e2) {
+ String message = String.format(
+ "Failed to create File 'res/%1$s/%2$s' : %3$s",
+ folderName, mFromFile.getName(), e2.getMessage());
+ AdtPlugin.displayError("Layout Creation", message);
+
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ message, e2);
+ } catch (CoreException e2) {
+ String message = String.format(
+ "Failed to create File 'res/%1$s/%2$s' : %3$s",
+ folderName, mFromFile.getName(), e2.getMessage());
+ AdtPlugin.displayError("Layout Creation", message);
+
+ return e2.getStatus();
+ }
+
+ return Status.OK_STATUS;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CustomViewFinder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CustomViewFinder.java
new file mode 100644
index 000000000..1f97c8c54
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CustomViewFinder.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2011 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 static com.android.SdkConstants.CLASS_VIEW;
+import static com.android.SdkConstants.CLASS_VIEWGROUP;
+import static com.android.SdkConstants.FN_FRAMEWORK_LIBRARY;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.core.Flags;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IMethod;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.core.search.IJavaSearchConstants;
+import org.eclipse.jdt.core.search.IJavaSearchScope;
+import org.eclipse.jdt.core.search.SearchEngine;
+import org.eclipse.jdt.core.search.SearchMatch;
+import org.eclipse.jdt.core.search.SearchParticipant;
+import org.eclipse.jdt.core.search.SearchPattern;
+import org.eclipse.jdt.core.search.SearchRequestor;
+import org.eclipse.jdt.internal.core.ResolvedBinaryType;
+import org.eclipse.jdt.internal.core.ResolvedSourceType;
+import org.eclipse.swt.widgets.Display;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The {@link CustomViewFinder} can look up the custom views and third party views
+ * available for a given project.
+ */
+@SuppressWarnings("restriction") // JDT model access for custom-view class lookup
+public class CustomViewFinder {
+ /**
+ * Qualified name for the per-project non-persistent property storing the
+ * {@link CustomViewFinder} for this project
+ */
+ private final static QualifiedName CUSTOM_VIEW_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ "viewfinder"); //$NON-NLS-1$
+
+ /** Project that this view finder locates views for */
+ private final IProject mProject;
+
+ private final List<Listener> mListeners = new ArrayList<Listener>();
+
+ private List<String> mCustomViews;
+ private List<String> mThirdPartyViews;
+ private boolean mRefreshing;
+
+ /**
+ * Constructs an {@link CustomViewFinder} for the given project. Don't use this method;
+ * use the {@link #get} factory method instead.
+ *
+ * @param project project to create an {@link CustomViewFinder} for
+ */
+ private CustomViewFinder(IProject project) {
+ mProject = project;
+ }
+
+ /**
+ * Returns the {@link CustomViewFinder} for the given project
+ *
+ * @param project the project the finder is associated with
+ * @return a {@CustomViewFinder} for the given project, never null
+ */
+ public static CustomViewFinder get(IProject project) {
+ CustomViewFinder finder = null;
+ try {
+ finder = (CustomViewFinder) project.getSessionProperty(CUSTOM_VIEW_FINDER);
+ } catch (CoreException e) {
+ // Not a problem; we will just create a new one
+ }
+
+ if (finder == null) {
+ finder = new CustomViewFinder(project);
+ try {
+ project.setSessionProperty(CUSTOM_VIEW_FINDER, finder);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't store CustomViewFinder");
+ }
+ }
+
+ return finder;
+ }
+
+ public void refresh() {
+ refresh(null /*listener*/, true /* sync */);
+ }
+
+ public void refresh(final Listener listener) {
+ refresh(listener, false /* sync */);
+ }
+
+ private void refresh(final Listener listener, boolean sync) {
+ // Add this listener to the list of listeners which should be notified when the
+ // search is done. (There could be more than one since multiple requests could
+ // arrive for a slow search since the search is run in a different thread).
+ if (listener != null) {
+ synchronized (this) {
+ mListeners.add(listener);
+ }
+ }
+ synchronized (this) {
+ if (listener != null) {
+ mListeners.add(listener);
+ }
+ if (mRefreshing) {
+ return;
+ }
+ mRefreshing = true;
+ }
+
+ FindViewsJob job = new FindViewsJob();
+ job.schedule();
+ if (sync) {
+ try {
+ job.join();
+ } catch (InterruptedException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ }
+
+ public Collection<String> getCustomViews() {
+ return mCustomViews == null ? null : Collections.unmodifiableCollection(mCustomViews);
+ }
+
+ public Collection<String> getThirdPartyViews() {
+ return mThirdPartyViews == null
+ ? null : Collections.unmodifiableCollection(mThirdPartyViews);
+ }
+
+ public Collection<String> getAllViews() {
+ // Not yet initialized: return null
+ if (mCustomViews == null) {
+ return null;
+ }
+ List<String> all = new ArrayList<String>(mCustomViews.size() + mThirdPartyViews.size());
+ all.addAll(mCustomViews);
+ all.addAll(mThirdPartyViews);
+ return all;
+ }
+
+ /**
+ * Returns a pair of view lists - the custom views and the 3rd-party views.
+ * This method performs no caching; it is the same as asking the custom view finder
+ * to refresh itself and then waiting for the answer and returning it.
+ *
+ * @param project the Android project
+ * @param layoutsOnly if true, only search for layouts
+ * @return a pair of lists, the first containing custom views and the second
+ * containing 3rd party views
+ */
+ public static Pair<List<String>,List<String>> findViews(
+ final IProject project, boolean layoutsOnly) {
+ CustomViewFinder finder = get(project);
+
+ return finder.findViews(layoutsOnly);
+ }
+
+ private Pair<List<String>,List<String>> findViews(final boolean layoutsOnly) {
+ final Set<String> customViews = new HashSet<String>();
+ final Set<String> thirdPartyViews = new HashSet<String>();
+
+ ProjectState state = Sdk.getProjectState(mProject);
+ final List<IProject> libraries = state != null
+ ? state.getFullLibraryProjects() : Collections.<IProject>emptyList();
+
+ SearchRequestor requestor = new SearchRequestor() {
+ @Override
+ public void acceptSearchMatch(SearchMatch match) throws CoreException {
+ // Ignore matches in comments
+ if (match.isInsideDocComment()) {
+ return;
+ }
+
+ Object element = match.getElement();
+ if (element instanceof ResolvedBinaryType) {
+ // Third party view
+ ResolvedBinaryType type = (ResolvedBinaryType) element;
+ IPackageFragment fragment = type.getPackageFragment();
+ IPath path = fragment.getPath();
+ String last = path.lastSegment();
+ // Filter out android.jar stuff
+ if (last.equals(FN_FRAMEWORK_LIBRARY)) {
+ return;
+ }
+ if (!isValidView(type, layoutsOnly)) {
+ return;
+ }
+
+ IProject matchProject = match.getResource().getProject();
+ if (mProject == matchProject || libraries.contains(matchProject)) {
+ String fqn = type.getFullyQualifiedName();
+ thirdPartyViews.add(fqn);
+ }
+ } else if (element instanceof ResolvedSourceType) {
+ // User custom view
+ IProject matchProject = match.getResource().getProject();
+ if (mProject == matchProject || libraries.contains(matchProject)) {
+ ResolvedSourceType type = (ResolvedSourceType) element;
+ if (!isValidView(type, layoutsOnly)) {
+ return;
+ }
+ String fqn = type.getFullyQualifiedName();
+ fqn = fqn.replace('$', '.');
+ customViews.add(fqn);
+ }
+ }
+ }
+ };
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject);
+ if (javaProject != null) {
+ String className = layoutsOnly ? CLASS_VIEWGROUP : CLASS_VIEW;
+ IType viewType = javaProject.findType(className);
+ if (viewType != null) {
+ IJavaSearchScope scope = SearchEngine.createHierarchyScope(viewType);
+ SearchParticipant[] participants = new SearchParticipant[] {
+ SearchEngine.getDefaultSearchParticipant()
+ };
+ int matchRule = SearchPattern.R_PATTERN_MATCH | SearchPattern.R_CASE_SENSITIVE;
+
+ SearchPattern pattern = SearchPattern.createPattern("*",
+ IJavaSearchConstants.CLASS, IJavaSearchConstants.IMPLEMENTORS,
+ matchRule);
+ SearchEngine engine = new SearchEngine();
+ engine.search(pattern, participants, scope, requestor,
+ new NullProgressMonitor());
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+
+ List<String> custom = new ArrayList<String>(customViews);
+ List<String> thirdParty = new ArrayList<String>(thirdPartyViews);
+
+ if (!layoutsOnly) {
+ // Update our cached answers (unless we were filtered on only layouts)
+ mCustomViews = custom;
+ mThirdPartyViews = thirdParty;
+ }
+
+ return Pair.of(custom, thirdParty);
+ }
+
+ /**
+ * Determines whether the given member is a valid android.view.View to be added to the
+ * list of custom views or third party views. It checks that the view is public and
+ * not abstract for example.
+ */
+ private static boolean isValidView(IType type, boolean layoutsOnly)
+ throws JavaModelException {
+ // Skip anonymous classes
+ if (type.isAnonymous()) {
+ return false;
+ }
+ int flags = type.getFlags();
+ if (Flags.isAbstract(flags) || !Flags.isPublic(flags)) {
+ return false;
+ }
+
+ // TODO: if (layoutsOnly) perhaps try to filter out AdapterViews and other ViewGroups
+ // not willing to accept children via XML
+
+ // See if the class has one of the acceptable constructors
+ // needed for XML instantiation:
+ // View(Context context)
+ // View(Context context, AttributeSet attrs)
+ // View(Context context, AttributeSet attrs, int defStyle)
+ // We don't simply do three direct checks via type.getMethod() because the types
+ // are not resolved, so we don't know for each parameter if we will get the
+ // fully qualified or the unqualified class names.
+ // Instead, iterate over the methods and look for a match.
+ String typeName = type.getElementName();
+ for (IMethod method : type.getMethods()) {
+ // Only care about constructors
+ if (!method.getElementName().equals(typeName)) {
+ continue;
+ }
+
+ String[] parameterTypes = method.getParameterTypes();
+ if (parameterTypes == null || parameterTypes.length < 1 || parameterTypes.length > 3) {
+ continue;
+ }
+
+ String first = parameterTypes[0];
+ // Look for the parameter type signatures -- produced by
+ // JDT's Signature.createTypeSignature("Context", false /*isResolved*/);.
+ // This is not a typo; they were copy/pasted from the actual parameter names
+ // observed in the debugger examining these data structures.
+ if (first.equals("QContext;") //$NON-NLS-1$
+ || first.equals("Qandroid.content.Context;")) { //$NON-NLS-1$
+ if (parameterTypes.length == 1) {
+ return true;
+ }
+ String second = parameterTypes[1];
+ if (second.equals("QAttributeSet;") //$NON-NLS-1$
+ || second.equals("Qandroid.util.AttributeSet;")) { //$NON-NLS-1$
+ if (parameterTypes.length == 2) {
+ return true;
+ }
+ String third = parameterTypes[2];
+ if (third.equals("I")) { //$NON-NLS-1$
+ if (parameterTypes.length == 3) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Interface implemented by clients of the {@link CustomViewFinder} to be notified
+ * when a custom view search has completed. Will always be called on the SWT event
+ * dispatch thread.
+ */
+ public interface Listener {
+ void viewsUpdated(Collection<String> customViews, Collection<String> thirdPartyViews);
+ }
+
+ /**
+ * Job for performing class search off the UI thread. This is marked as a system job
+ * so that it won't show up in the progress monitor etc.
+ */
+ private class FindViewsJob extends Job {
+ FindViewsJob() {
+ super("Find Custom Views");
+ setSystem(true);
+ }
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ Pair<List<String>, List<String>> views = findViews(false);
+ mCustomViews = views.getFirst();
+ mThirdPartyViews = views.getSecond();
+
+ // Notify listeners on SWT's UI thread
+ Display.getDefault().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ Collection<String> customViews =
+ Collections.unmodifiableCollection(mCustomViews);
+ Collection<String> thirdPartyViews =
+ Collections.unmodifiableCollection(mThirdPartyViews);
+ synchronized (this) {
+ for (Listener l : mListeners) {
+ l.viewsUpdated(customViews, thirdPartyViews);
+ }
+ mListeners.clear();
+ mRefreshing = false;
+ }
+ }
+ });
+ return Status.OK_STATUS;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DelegatingAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DelegatingAction.java
new file mode 100644
index 000000000..7a41b5b15
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DelegatingAction.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2012 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.annotations.NonNull;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IMenuCreator;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.util.IPropertyChangeListener;
+import org.eclipse.swt.events.HelpListener;
+import org.eclipse.swt.widgets.Event;
+
+/**
+ * Implementation of {@link IAction} which delegates to a different
+ * {@link IAction} which allows a subclass to wrap and customize some of the
+ * behavior of a different action
+ */
+public class DelegatingAction implements IAction {
+ private final IAction mAction;
+
+ /**
+ * Construct a new delegate of the given action
+ *
+ * @param action the action to be delegated
+ */
+ public DelegatingAction(@NonNull IAction action) {
+ mAction = action;
+ }
+
+ @Override
+ public void addPropertyChangeListener(IPropertyChangeListener listener) {
+ mAction.addPropertyChangeListener(listener);
+ }
+
+ @Override
+ public int getAccelerator() {
+ return mAction.getAccelerator();
+ }
+
+ @Override
+ public String getActionDefinitionId() {
+ return mAction.getActionDefinitionId();
+ }
+
+ @Override
+ public String getDescription() {
+ return mAction.getDescription();
+ }
+
+ @Override
+ public ImageDescriptor getDisabledImageDescriptor() {
+ return mAction.getDisabledImageDescriptor();
+ }
+
+ @Override
+ public HelpListener getHelpListener() {
+ return mAction.getHelpListener();
+ }
+
+ @Override
+ public ImageDescriptor getHoverImageDescriptor() {
+ return mAction.getHoverImageDescriptor();
+ }
+
+ @Override
+ public String getId() {
+ return mAction.getId();
+ }
+
+ @Override
+ public ImageDescriptor getImageDescriptor() {
+ return mAction.getImageDescriptor();
+ }
+
+ @Override
+ public IMenuCreator getMenuCreator() {
+ return mAction.getMenuCreator();
+ }
+
+ @Override
+ public int getStyle() {
+ return mAction.getStyle();
+ }
+
+ @Override
+ public String getText() {
+ return mAction.getText();
+ }
+
+ @Override
+ public String getToolTipText() {
+ return mAction.getToolTipText();
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mAction.isChecked();
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mAction.isEnabled();
+ }
+
+ @Override
+ public boolean isHandled() {
+ return mAction.isHandled();
+ }
+
+ @Override
+ public void removePropertyChangeListener(IPropertyChangeListener listener) {
+ mAction.removePropertyChangeListener(listener);
+ }
+
+ @Override
+ public void run() {
+ mAction.run();
+ }
+
+ @Override
+ public void runWithEvent(Event event) {
+ mAction.runWithEvent(event);
+ }
+
+ @Override
+ public void setActionDefinitionId(String id) {
+ mAction.setActionDefinitionId(id);
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ mAction.setChecked(checked);
+ }
+
+ @Override
+ public void setDescription(String text) {
+ mAction.setDescription(text);
+ }
+
+ @Override
+ public void setDisabledImageDescriptor(ImageDescriptor newImage) {
+ mAction.setDisabledImageDescriptor(newImage);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ mAction.setEnabled(enabled);
+ }
+
+ @Override
+ public void setHelpListener(HelpListener listener) {
+ mAction.setHelpListener(listener);
+ }
+
+ @Override
+ public void setHoverImageDescriptor(ImageDescriptor newImage) {
+ mAction.setHoverImageDescriptor(newImage);
+ }
+
+ @Override
+ public void setId(String id) {
+ mAction.setId(id);
+ }
+
+ @Override
+ public void setImageDescriptor(ImageDescriptor newImage) {
+ mAction.setImageDescriptor(newImage);
+ }
+
+ @Override
+ public void setMenuCreator(IMenuCreator creator) {
+ mAction.setMenuCreator(creator);
+ }
+
+ @Override
+ public void setText(String text) {
+ mAction.setText(text);
+ }
+
+ @Override
+ public void setToolTipText(String text) {
+ mAction.setToolTipText(text);
+ }
+
+ @Override
+ public void setAccelerator(int keycode) {
+ mAction.setAccelerator(keycode);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java
new file mode 100644
index 000000000..145036bf3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java
@@ -0,0 +1,915 @@
+/*
+ * Copyright (C) 2011 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 static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.TOOLS_URI;
+import static org.eclipse.wst.xml.core.internal.provisional.contenttype.ContentTypeIdForXML.ContentTypeID_XML;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+/**
+ * Various utility methods for manipulating DOM nodes.
+ */
+@SuppressWarnings("restriction") // No replacement for restricted XML model yet
+public class DomUtilities {
+ /**
+ * Finds the nearest common parent of the two given nodes (which could be one of the
+ * two nodes as well)
+ *
+ * @param node1 the first node to test
+ * @param node2 the second node to test
+ * @return the nearest common parent of the two given nodes
+ */
+ @Nullable
+ public static Node getCommonAncestor(@NonNull Node node1, @NonNull Node node2) {
+ while (node2 != null) {
+ Node current = node1;
+ while (current != null && current != node2) {
+ current = current.getParentNode();
+ }
+ if (current == node2) {
+ return current;
+ }
+ node2 = node2.getParentNode();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns all elements below the given node (which can be a document,
+ * element, etc). This will include the node itself, if it is an element.
+ *
+ * @param node the node to search from
+ * @return all elements in the subtree formed by the node parameter
+ */
+ @NonNull
+ public static List<Element> getAllElements(@NonNull Node node) {
+ List<Element> elements = new ArrayList<Element>(64);
+ addElements(node, elements);
+ return elements;
+ }
+
+ private static void addElements(@NonNull Node node, @NonNull List<Element> elements) {
+ if (node instanceof Element) {
+ elements.add((Element) node);
+ }
+
+ NodeList childNodes = node.getChildNodes();
+ for (int i = 0, n = childNodes.getLength(); i < n; i++) {
+ addElements(childNodes.item(i), elements);
+ }
+ }
+
+ /**
+ * Returns the depth of the given node (with the document node having depth 0,
+ * and the document element having depth 1)
+ *
+ * @param node the node to test
+ * @return the depth in the document
+ */
+ public static int getDepth(@NonNull Node node) {
+ int depth = -1;
+ while (node != null) {
+ depth++;
+ node = node.getParentNode();
+ }
+
+ return depth;
+ }
+
+ /**
+ * Returns true if the given node has one or more element children
+ *
+ * @param node the node to test for element children
+ * @return true if the node has one or more element children
+ */
+ public static boolean hasElementChildren(@NonNull Node node) {
+ NodeList children = node.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ if (children.item(i).getNodeType() == Node.ELEMENT_NODE) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the DOM document for the given file
+ *
+ * @param file the XML file
+ * @return the document, or null if not found or not parsed properly (no
+ * errors are generated/thrown)
+ */
+ @Nullable
+ public static Document getDocument(@NonNull IFile file) {
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ if (modelManager == null) {
+ return null;
+ }
+ try {
+ IStructuredModel model = modelManager.getExistingModelForRead(file);
+ if (model == null) {
+ model = modelManager.getModelForRead(file);
+ }
+ if (model != null) {
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ return domModel.getDocument();
+ }
+ try {
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+ } catch (Exception e) {
+ // Ignore exceptions.
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the DOM document for the given editor
+ *
+ * @param editor the XML editor
+ * @return the document, or null if not found or not parsed properly (no
+ * errors are generated/thrown)
+ */
+ @Nullable
+ public static Document getDocument(@NonNull AndroidXmlEditor editor) {
+ IStructuredModel model = editor.getModelForRead();
+ try {
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ return domModel.getDocument();
+ }
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Returns the XML DOM node corresponding to the given offset of the given
+ * document.
+ *
+ * @param document The document to look in
+ * @param offset The offset to look up the node for
+ * @return The node containing the offset, or null
+ */
+ @Nullable
+ public static Node getNode(@NonNull IDocument document, int offset) {
+ Node node = null;
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ if (modelManager == null) {
+ return null;
+ }
+ try {
+ IStructuredModel model = modelManager.getExistingModelForRead(document);
+ if (model != null) {
+ try {
+ for (; offset >= 0 && node == null; --offset) {
+ node = (Node) model.getIndexedRegion(offset);
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+ } catch (Exception e) {
+ // Ignore exceptions.
+ }
+
+ return node;
+ }
+
+ /**
+ * Returns the editing context at the given offset, as a pair of parent node and child
+ * node. This is not the same as just calling {@link DomUtilities#getNode} and taking
+ * its parent node, because special care has to be taken to return content element
+ * positions.
+ * <p>
+ * For example, for the XML {@code <foo>^</foo>}, if the caret ^ is inside the foo
+ * element, between the opening and closing tags, then the foo element is the parent,
+ * and the child is null which represents a potential text node.
+ * <p>
+ * If the node is inside an element tag definition (between the opening and closing
+ * bracket) then the child node will be the element and whatever parent (element or
+ * document) will be its parent.
+ * <p>
+ * If the node is in a text node, then the text node will be the child and its parent
+ * element or document node its parent.
+ * <p>
+ * Finally, if the caret is on a boundary of a text node, then the text node will be
+ * considered the child, regardless of whether it is on the left or right of the
+ * caret. For example, in the XML {@code <foo>^ </foo>} and in the XML
+ * {@code <foo> ^</foo>}, in both cases the text node is preferred over the element.
+ *
+ * @param document the document to search in
+ * @param offset the offset to look up
+ * @return a pair of parent and child elements, where either the parent or the child
+ * but not both can be null, and if non null the child.getParentNode() should
+ * return the parent. Note that the method can also return null if no
+ * document or model could be obtained or if the offset is invalid.
+ */
+ @Nullable
+ public static Pair<Node, Node> getNodeContext(@NonNull IDocument document, int offset) {
+ Node node = null;
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ if (modelManager == null) {
+ return null;
+ }
+ try {
+ IStructuredModel model = modelManager.getExistingModelForRead(document);
+ if (model != null) {
+ try {
+ for (; offset >= 0 && node == null; --offset) {
+ IndexedRegion indexedRegion = model.getIndexedRegion(offset);
+ if (indexedRegion != null) {
+ node = (Node) indexedRegion;
+
+ if (node.getNodeType() == Node.TEXT_NODE) {
+ return Pair.of(node.getParentNode(), node);
+ }
+
+ // Look at the structured document to see if
+ // we have the special case where the caret is pointing at
+ // a -potential- text node, e.g. <foo>^</foo>
+ IStructuredDocument doc = model.getStructuredDocument();
+ IStructuredDocumentRegion region =
+ doc.getRegionAtCharacterOffset(offset);
+
+ ITextRegion subRegion = region.getRegionAtCharacterOffset(offset);
+ String type = subRegion.getType();
+ if (DOMRegionContext.XML_END_TAG_OPEN.equals(type)) {
+ // Try to return the text node if it's on the left
+ // of this element node, such that replace strings etc
+ // can be computed.
+ Node lastChild = node.getLastChild();
+ if (lastChild != null) {
+ IndexedRegion previousRegion = (IndexedRegion) lastChild;
+ if (previousRegion.getEndOffset() == offset) {
+ return Pair.of(node, lastChild);
+ }
+ }
+ return Pair.of(node, null);
+ }
+
+ return Pair.of(node.getParentNode(), node);
+ }
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+ } catch (Exception e) {
+ // Ignore exceptions.
+ }
+
+ return null;
+ }
+
+ /**
+ * Like {@link #getNode(IDocument, int)}, but has a bias parameter which lets you
+ * indicate whether you want the search to look forwards or backwards.
+ * This is vital when trying to compute a node range. Consider the following
+ * XML fragment:
+ * {@code
+ * <a/><b/>[<c/><d/><e/>]<f/><g/>
+ * }
+ * Suppose we want to locate the nodes in the range indicated by the brackets above.
+ * If we want to search for the node corresponding to the start position, should
+ * we pick the node on its left or the node on its right? Similarly for the end
+ * position. Clearly, we'll need to bias the search towards the right when looking
+ * for the start position, and towards the left when looking for the end position.
+ * The following method lets us do just that. When passed an offset which sits
+ * on the edge of the computed node, it will pick the neighbor based on whether
+ * "forward" is true or false, where forward means searching towards the right
+ * and not forward is obviously towards the left.
+ * @param document the document to search in
+ * @param offset the offset to search for
+ * @param forward if true, search forwards, otherwise search backwards when on node boundaries
+ * @return the node which surrounds the given offset, or the node adjacent to the offset
+ * where the side depends on the forward parameter
+ */
+ @Nullable
+ public static Node getNode(@NonNull IDocument document, int offset, boolean forward) {
+ Node node = getNode(document, offset);
+
+ if (node instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) node;
+
+ if (!forward && offset <= region.getStartOffset()) {
+ Node left = node.getPreviousSibling();
+ if (left == null) {
+ left = node.getParentNode();
+ }
+
+ node = left;
+ } else if (forward && offset >= region.getEndOffset()) {
+ Node right = node.getNextSibling();
+ if (right == null) {
+ right = node.getParentNode();
+ }
+ node = right;
+ }
+ }
+
+ return node;
+ }
+
+ /**
+ * Returns a range of elements for the given caret range. Note that the two elements
+ * may not be at the same level so callers may want to perform additional input
+ * filtering.
+ *
+ * @param document the document to search in
+ * @param beginOffset the beginning offset of the range
+ * @param endOffset the ending offset of the range
+ * @return a pair of begin+end elements, or null
+ */
+ @Nullable
+ public static Pair<Element, Element> getElementRange(@NonNull IDocument document,
+ int beginOffset, int endOffset) {
+ Element beginElement = null;
+ Element endElement = null;
+ Node beginNode = getNode(document, beginOffset, true);
+ Node endNode = beginNode;
+ if (endOffset > beginOffset) {
+ endNode = getNode(document, endOffset, false);
+ }
+
+ if (beginNode == null || endNode == null) {
+ return null;
+ }
+
+ // Adjust offsets if you're pointing at text
+ if (beginNode.getNodeType() != Node.ELEMENT_NODE) {
+ // <foo> <bar1/> | <bar2/> </foo> => should pick <bar2/>
+ beginElement = getNextElement(beginNode);
+ if (beginElement == null) {
+ // Might be inside the end of a parent, e.g.
+ // <foo> <bar/> | </foo> => should pick <bar/>
+ beginElement = getPreviousElement(beginNode);
+ if (beginElement == null) {
+ // We must be inside an empty element,
+ // <foo> | </foo>
+ // In that case just pick the parent.
+ beginElement = getParentElement(beginNode);
+ }
+ }
+ } else {
+ beginElement = (Element) beginNode;
+ }
+
+ if (endNode.getNodeType() != Node.ELEMENT_NODE) {
+ // In the following, | marks the caret position:
+ // <foo> <bar1/> | <bar2/> </foo> => should pick <bar1/>
+ endElement = getPreviousElement(endNode);
+ if (endElement == null) {
+ // Might be inside the beginning of a parent, e.g.
+ // <foo> | <bar/></foo> => should pick <bar/>
+ endElement = getNextElement(endNode);
+ if (endElement == null) {
+ // We must be inside an empty element,
+ // <foo> | </foo>
+ // In that case just pick the parent.
+ endElement = getParentElement(endNode);
+ }
+ }
+ } else {
+ endElement = (Element) endNode;
+ }
+
+ if (beginElement != null && endElement != null) {
+ return Pair.of(beginElement, endElement);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the next sibling element of the node, or null if there is no such element
+ *
+ * @param node the starting node
+ * @return the next sibling element, or null
+ */
+ @Nullable
+ public static Element getNextElement(@NonNull Node node) {
+ while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
+ node = node.getNextSibling();
+ }
+
+ return (Element) node; // may be null as well
+ }
+
+ /**
+ * Returns the previous sibling element of the node, or null if there is no such element
+ *
+ * @param node the starting node
+ * @return the previous sibling element, or null
+ */
+ @Nullable
+ public static Element getPreviousElement(@NonNull Node node) {
+ while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
+ node = node.getPreviousSibling();
+ }
+
+ return (Element) node; // may be null as well
+ }
+
+ /**
+ * Returns the closest ancestor element, or null if none
+ *
+ * @param node the starting node
+ * @return the closest parent element, or null
+ */
+ @Nullable
+ public static Element getParentElement(@NonNull Node node) {
+ while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
+ node = node.getParentNode();
+ }
+
+ return (Element) node; // may be null as well
+ }
+
+ /** Utility used by {@link #getFreeWidgetId(Element)} */
+ private static void addLowercaseIds(@NonNull Element root, @NonNull Set<String> seen) {
+ if (root.hasAttributeNS(ANDROID_URI, ATTR_ID)) {
+ String id = root.getAttributeNS(ANDROID_URI, ATTR_ID);
+ if (id.startsWith(NEW_ID_PREFIX)) {
+ // See getFreeWidgetId for details on locale
+ seen.add(id.substring(NEW_ID_PREFIX.length()).toLowerCase(Locale.US));
+ } else if (id.startsWith(ID_PREFIX)) {
+ seen.add(id.substring(ID_PREFIX.length()).toLowerCase(Locale.US));
+ } else {
+ seen.add(id.toLowerCase(Locale.US));
+ }
+ }
+ }
+
+ /**
+ * Returns a suitable new widget id (not including the {@code @id/} prefix) for the
+ * given element, which is guaranteed to be unique in this document
+ *
+ * @param element the element to compute a new widget id for
+ * @param reserved an optional set of extra, "reserved" set of ids that should be
+ * considered taken
+ * @param prefix an optional prefix to use for the generated name, or null to get a
+ * default (which is currently the tag name)
+ * @return a unique id, never null, which does not include the {@code @id/} prefix
+ * @see DescriptorsUtils#getFreeWidgetId
+ */
+ public static String getFreeWidgetId(
+ @NonNull Element element,
+ @Nullable Set<String> reserved,
+ @Nullable String prefix) {
+ Set<String> ids = new HashSet<String>();
+ if (reserved != null) {
+ for (String id : reserved) {
+ // Note that we perform locale-independent lowercase checks; in "Image" we
+ // want the lowercase version to be "image", not "?mage" where ? is
+ // the char LATIN SMALL LETTER DOTLESS I.
+
+ ids.add(id.toLowerCase(Locale.US));
+ }
+ }
+ addLowercaseIds(element.getOwnerDocument().getDocumentElement(), ids);
+
+ if (prefix == null) {
+ prefix = DescriptorsUtils.getBasename(element.getTagName());
+ }
+ String generated;
+ int num = 1;
+ do {
+ generated = String.format("%1$s%2$d", prefix, num++); //$NON-NLS-1$
+ } while (ids.contains(generated.toLowerCase(Locale.US)));
+
+ return generated;
+ }
+
+ /**
+ * Returns the element children of the given element
+ *
+ * @param element the parent element
+ * @return a list of child elements, possibly empty but never null
+ */
+ @NonNull
+ public static List<Element> getChildren(@NonNull Element element) {
+ // Convenience to avoid lots of ugly DOM access casting
+ NodeList children = element.getChildNodes();
+ // An iterator would have been more natural (to directly drive the child list
+ // iteration) but iterators can't be used in enhanced for loops...
+ List<Element> result = new ArrayList<Element>(children.getLength());
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element child = (Element) node;
+ result.add(child);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns true iff the given elements are contiguous siblings
+ *
+ * @param elements the elements to be tested
+ * @return true if the elements are contiguous siblings with no gaps
+ */
+ public static boolean isContiguous(@NonNull List<Element> elements) {
+ if (elements.size() > 1) {
+ // All elements must be siblings (e.g. same parent)
+ Node parent = elements.get(0).getParentNode();
+ if (!(parent instanceof Element)) {
+ return false;
+ }
+ for (Element node : elements) {
+ if (parent != node.getParentNode()) {
+ return false;
+ }
+ }
+
+ // Ensure that the siblings are contiguous; no gaps.
+ // If we've selected all the children of the parent then we don't need
+ // to look.
+ List<Element> siblings = DomUtilities.getChildren((Element) parent);
+ if (siblings.size() != elements.size()) {
+ Set<Element> nodeSet = new HashSet<Element>(elements);
+ boolean inRange = false;
+ int remaining = elements.size();
+ for (Element node : siblings) {
+ boolean in = nodeSet.contains(node);
+ if (in) {
+ remaining--;
+ if (remaining == 0) {
+ break;
+ }
+ inRange = true;
+ } else if (inRange) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Determines whether two element trees are equivalent. Two element trees are
+ * equivalent if they represent the same DOM structure (elements, attributes, and
+ * children in order). This is almost the same as simply checking whether the String
+ * representations of the two nodes are identical, but this allows for minor
+ * variations that are not semantically significant, such as variations in formatting
+ * or ordering of the element attribute declarations, and the text children are
+ * ignored (this is such that in for example layout where content is only used for
+ * indentation the indentation differences are ignored). Null trees are never equal.
+ *
+ * @param element1 the first element to compare
+ * @param element2 the second element to compare
+ * @return true if the two element hierarchies are logically equal
+ */
+ public static boolean isEquivalent(@Nullable Element element1, @Nullable Element element2) {
+ if (element1 == null || element2 == null) {
+ return false;
+ }
+
+ if (!element1.getTagName().equals(element2.getTagName())) {
+ return false;
+ }
+
+ // Check attribute map
+ NamedNodeMap attributes1 = element1.getAttributes();
+ NamedNodeMap attributes2 = element2.getAttributes();
+
+ List<Attr> attributeNodes1 = new ArrayList<Attr>();
+ for (int i = 0, n = attributes1.getLength(); i < n; i++) {
+ Attr attribute = (Attr) attributes1.item(i);
+ // Ignore tools uri namespace attributes for equivalency test
+ if (TOOLS_URI.equals(attribute.getNamespaceURI())) {
+ continue;
+ }
+ attributeNodes1.add(attribute);
+ }
+ List<Attr> attributeNodes2 = new ArrayList<Attr>();
+ for (int i = 0, n = attributes2.getLength(); i < n; i++) {
+ Attr attribute = (Attr) attributes2.item(i);
+ // Ignore tools uri namespace attributes for equivalency test
+ if (TOOLS_URI.equals(attribute.getNamespaceURI())) {
+ continue;
+ }
+ attributeNodes2.add(attribute);
+ }
+
+ if (attributeNodes1.size() != attributeNodes2.size()) {
+ return false;
+ }
+
+ if (attributes1.getLength() > 0) {
+ Collections.sort(attributeNodes1, ATTRIBUTE_COMPARATOR);
+ Collections.sort(attributeNodes2, ATTRIBUTE_COMPARATOR);
+ for (int i = 0; i < attributeNodes1.size(); i++) {
+ Attr attr1 = attributeNodes1.get(i);
+ Attr attr2 = attributeNodes2.get(i);
+ if (attr1.getLocalName() == null || attr2.getLocalName() == null) {
+ if (!attr1.getName().equals(attr2.getName())) {
+ return false;
+ }
+ } else if (!attr1.getLocalName().equals(attr2.getLocalName())) {
+ return false;
+ }
+ if (!attr1.getValue().equals(attr2.getValue())) {
+ return false;
+ }
+ if (attr1.getNamespaceURI() == null) {
+ if (attr2.getNamespaceURI() != null) {
+ return false;
+ }
+ } else if (attr2.getNamespaceURI() == null) {
+ return false;
+ } else if (!attr1.getNamespaceURI().equals(attr2.getNamespaceURI())) {
+ return false;
+ }
+ }
+ }
+
+ NodeList children1 = element1.getChildNodes();
+ NodeList children2 = element2.getChildNodes();
+ int nextIndex1 = 0;
+ int nextIndex2 = 0;
+ while (true) {
+ while (nextIndex1 < children1.getLength() &&
+ children1.item(nextIndex1).getNodeType() != Node.ELEMENT_NODE) {
+ nextIndex1++;
+ }
+
+ while (nextIndex2 < children2.getLength() &&
+ children2.item(nextIndex2).getNodeType() != Node.ELEMENT_NODE) {
+ nextIndex2++;
+ }
+
+ Element nextElement1 = (Element) (nextIndex1 < children1.getLength()
+ ? children1.item(nextIndex1) : null);
+ Element nextElement2 = (Element) (nextIndex2 < children2.getLength()
+ ? children2.item(nextIndex2) : null);
+ if (nextElement1 == null) {
+ return nextElement2 == null;
+ } else if (nextElement2 == null) {
+ return false;
+ } else if (!isEquivalent(nextElement1, nextElement2)) {
+ return false;
+ }
+ nextIndex1++;
+ nextIndex2++;
+ }
+ }
+
+ /**
+ * Finds the corresponding element in a document to a given element in another
+ * document. Note that this does <b>not</b> do any kind of equivalence check
+ * (see {@link #isEquivalent(Element, Element)}), and currently the search
+ * is only by id; there is no structural search.
+ *
+ * @param element the element to find an equivalent for
+ * @param document the document to search for an equivalent element in
+ * @return an equivalent element, or null
+ */
+ @Nullable
+ public static Element findCorresponding(@NonNull Element element, @NonNull Document document) {
+ // Make sure the method is called correctly -- the element is for a different
+ // document than the one we are searching
+ assert element.getOwnerDocument() != document;
+
+ // First search by id. This allows us to find the corresponding
+ String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
+ if (id != null && id.length() > 0) {
+ if (id.startsWith(ID_PREFIX)) {
+ id = NEW_ID_PREFIX + id.substring(ID_PREFIX.length());
+ }
+
+ return findCorresponding(document.getDocumentElement(), id);
+ }
+
+ // TODO: Search by structure - look in the document and
+ // find a corresponding element in the same location in the structure,
+ // e.g. 4th child of root, 3rd child, 6th child, then pick node with tag "foo".
+
+ return null;
+ }
+
+ /** Helper method for {@link #findCorresponding(Element, Document)} */
+ @Nullable
+ private static Element findCorresponding(@NonNull Element element, @NonNull String targetId) {
+ String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
+ if (id != null) { // Work around DOM bug
+ if (id.equals(targetId)) {
+ return element;
+ } else if (id.startsWith(ID_PREFIX)) {
+ id = NEW_ID_PREFIX + id.substring(ID_PREFIX.length());
+ if (id.equals(targetId)) {
+ return element;
+ }
+ }
+ }
+
+ NodeList children = element.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element child = (Element) node;
+ Element match = findCorresponding(child, targetId);
+ if (match != null) {
+ return match;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses the given XML string as a DOM document, using Eclipse's structured
+ * XML model (which for example allows us to distinguish empty elements
+ * (<foo/>) from elements with no children (<foo></foo>).
+ *
+ * @param xml the XML content to be parsed (must be well formed)
+ * @return the DOM document, or null
+ */
+ @Nullable
+ public static Document parseStructuredDocument(@NonNull String xml) {
+ IStructuredModel model = createStructuredModel(xml);
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ return domModel.getDocument();
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses the given XML string and builds an Eclipse structured model for it.
+ *
+ * @param xml the XML content to be parsed (must be well formed)
+ * @return the structured model
+ */
+ @Nullable
+ public static IStructuredModel createStructuredModel(@NonNull String xml) {
+ IStructuredModel model = createEmptyModel();
+ IStructuredDocument document = model.getStructuredDocument();
+ model.aboutToChangeModel();
+ document.set(xml);
+ model.changedModel();
+
+ return model;
+ }
+
+ /**
+ * Creates an empty Eclipse XML model
+ *
+ * @return a new Eclipse XML model
+ */
+ @NonNull
+ public static IStructuredModel createEmptyModel() {
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ return modelManager.createUnManagedStructuredModelFor(ContentTypeID_XML);
+ }
+
+ /**
+ * Creates an empty Eclipse XML document
+ *
+ * @return an empty Eclipse XML document
+ */
+ @Nullable
+ public static Document createEmptyDocument() {
+ IStructuredModel model = createEmptyModel();
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ return domModel.getDocument();
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates an empty non-Eclipse XML document.
+ * This is used when you need to use XML operations not supported by
+ * the Eclipse XML model (such as serialization).
+ * <p>
+ * The new document will not validate, will ignore comments, and will
+ * support namespace.
+ *
+ * @return the new document
+ */
+ @Nullable
+ public static Document createEmptyPlainDocument() {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ factory.setIgnoringComments(true);
+ DocumentBuilder builder;
+ try {
+ builder = factory.newDocumentBuilder();
+ return builder.newDocument();
+ } catch (ParserConfigurationException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses the given XML string as a DOM document, using the JDK parser.
+ * The parser does not validate, and is namespace aware.
+ *
+ * @param xml the XML content to be parsed (must be well formed)
+ * @param logParserErrors if true, log parser errors to the log, otherwise
+ * silently return null
+ * @return the DOM document, or null
+ */
+ @Nullable
+ public static Document parseDocument(@NonNull String xml, boolean logParserErrors) {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ InputSource is = new InputSource(new StringReader(xml));
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ try {
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ return builder.parse(is);
+ } catch (Exception e) {
+ if (logParserErrors) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ return null;
+ }
+
+ /** Can be used to sort attributes by name */
+ private static final Comparator<Attr> ATTRIBUTE_COMPARATOR = new Comparator<Attr>() {
+ @Override
+ public int compare(Attr a1, Attr a2) {
+ return a1.getName().compareTo(a2.getName());
+ }
+ };
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DropGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DropGesture.java
new file mode 100644
index 000000000..bb3be7f68
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DropGesture.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2010 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 org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.DropTargetListener;
+
+/**
+ * A {@link DropGesture} is a {@link Gesture} which deals with drag and drop, so
+ * it has additional hooks for indicating whether the current position is
+ * "valid", and in general gets access to the system drag and drop data
+ * structures. See the {@link Gesture} documentation for more details on whether
+ * you should choose a plain {@link Gesture} or a {@link DropGesture}.
+ */
+public abstract class DropGesture extends Gesture {
+ /**
+ * The cursor has entered the drop target boundaries.
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#dragEnter(DropTargetEvent)
+ */
+ public void dragEnter(DropTargetEvent event) {
+ }
+
+ /**
+ * The cursor is moving over the drop target.
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#dragOver(DropTargetEvent)
+ */
+ public void dragOver(DropTargetEvent event) {
+ }
+
+ /**
+ * The operation being performed has changed (usually due to the user
+ * changing the selected modifier key(s) while dragging).
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#dragOperationChanged(DropTargetEvent)
+ */
+ public void dragOperationChanged(DropTargetEvent event) {
+ }
+
+ /**
+ * The cursor has left the drop target boundaries OR the drop has been
+ * canceled OR the data is about to be dropped.
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#dragLeave(DropTargetEvent)
+ */
+ public void dragLeave(DropTargetEvent event) {
+ }
+
+ /**
+ * The drop is about to be performed. The drop target is given a last chance
+ * to change the nature of the drop.
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#dropAccept(DropTargetEvent)
+ */
+ public void dropAccept(DropTargetEvent event) {
+ }
+
+ /**
+ * The data is being dropped. The data field contains java format of the
+ * data being dropped.
+ *
+ * @param event The {@link DropTargetEvent} for this drag and drop event
+ * @see DropTargetListener#drop(DropTargetEvent)
+ */
+ public void drop(final DropTargetEvent event) {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java
new file mode 100644
index 000000000..fc7127278
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java
@@ -0,0 +1,654 @@
+/*
+ * Copyright (C) 2010 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 static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
+import static com.android.SdkConstants.FQCN_GESTURE_OVERLAY_VIEW;
+import static com.android.SdkConstants.FQCN_IMAGE_VIEW;
+import static com.android.SdkConstants.FQCN_LINEAR_LAYOUT;
+import static com.android.SdkConstants.FQCN_TEXT_VIEW;
+import static com.android.SdkConstants.GRID_VIEW;
+import static com.android.SdkConstants.LIST_VIEW;
+import static com.android.SdkConstants.SPINNER;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+
+import com.android.SdkConstants;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.api.RuleAction.Choices;
+import com.android.ide.common.api.RuleAction.NestedAction;
+import com.android.ide.common.api.RuleAction.Toggle;
+import com.android.ide.common.layout.BaseViewRule;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeLayoutAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeViewAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ExtractIncludeAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ExtractStyleAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.UnwrapAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.UseCompoundDrawableAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.WrapInAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.ContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IContributionItem;
+import org.eclipse.jface.action.IMenuListener;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Menu;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Helper class that is responsible for adding and managing the dynamic menu items
+ * contributed by the {@link IViewRule} instances, based on the current selection
+ * on the {@link LayoutCanvas}.
+ * <p/>
+ * This class is tied to a specific {@link LayoutCanvas} instance and a root {@link MenuManager}.
+ * <p/>
+ * Two instances of this are used: one created by {@link LayoutCanvas} and the other one
+ * created by {@link OutlinePage}. Different root {@link MenuManager}s are populated, however
+ * they are both linked to the current selection state of the {@link LayoutCanvas}.
+ */
+class DynamicContextMenu {
+ public static String DEFAULT_ACTION_SHORTCUT = "F2"; //$NON-NLS-1$
+ public static int DEFAULT_ACTION_KEY = SWT.F2;
+
+ /** The XML layout editor that contains the canvas that uses this menu. */
+ private final LayoutEditorDelegate mEditorDelegate;
+
+ /** The layout canvas that displays this context menu. */
+ private final LayoutCanvas mCanvas;
+
+ /** The root menu manager of the context menu. */
+ private final MenuManager mMenuManager;
+
+ /**
+ * Creates a new helper responsible for adding and managing the dynamic menu items
+ * contributed by the {@link IViewRule} instances, based on the current selection
+ * on the {@link LayoutCanvas}.
+ * @param editorDelegate the editor owning the menu
+ * @param canvas The {@link LayoutCanvas} providing the selection, the node factory and
+ * the rules engine.
+ * @param rootMenu The root of the context menu displayed. In practice this may be the
+ * context menu manager of the {@link LayoutCanvas} or the one from {@link OutlinePage}.
+ */
+ public DynamicContextMenu(
+ LayoutEditorDelegate editorDelegate,
+ LayoutCanvas canvas,
+ MenuManager rootMenu) {
+ mEditorDelegate = editorDelegate;
+ mCanvas = canvas;
+ mMenuManager = rootMenu;
+
+ setupDynamicMenuActions();
+ }
+
+ /**
+ * Setups the menu manager to receive dynamic menu contributions from the {@link IViewRule}s
+ * when it's about to be shown.
+ */
+ private void setupDynamicMenuActions() {
+ // Remember how many static actions we have. Then each time the menu is
+ // shown, find dynamic contributions based on the current selection and insert
+ // them at the beginning of the menu.
+ final int numStaticActions = mMenuManager.getSize();
+ mMenuManager.addMenuListener(new IMenuListener() {
+ @Override
+ public void menuAboutToShow(IMenuManager manager) {
+
+ // Remove any previous dynamic contributions to keep only the
+ // default static items.
+ int n = mMenuManager.getSize() - numStaticActions;
+ if (n > 0) {
+ IContributionItem[] items = mMenuManager.getItems();
+ for (int i = 0; i < n; i++) {
+ mMenuManager.remove(items[i]);
+ }
+ }
+
+ // Now add all the dynamic menu actions depending on the current selection.
+ populateDynamicContextMenu();
+ }
+ });
+
+ }
+
+ /**
+ * This method is invoked by <code>menuAboutToShow</code> on {@link #mMenuManager}.
+ * All previous dynamic menu actions have been removed and this method can now insert
+ * any new actions that depend on the current selection.
+ */
+ private void populateDynamicContextMenu() {
+ // Create the actual menu contributions
+ String endId = mMenuManager.getItems()[0].getId();
+
+ Separator sep = new Separator();
+ sep.setId("-dyn-gle-sep"); //$NON-NLS-1$
+ mMenuManager.insertBefore(endId, sep);
+ endId = sep.getId();
+
+ List<SelectionItem> selections = mCanvas.getSelectionManager().getSelections();
+ if (selections.size() == 0) {
+ return;
+ }
+ List<INode> nodes = new ArrayList<INode>(selections.size());
+ for (SelectionItem item : selections) {
+ nodes.add(item.getNode());
+ }
+
+ List<IContributionItem> menuItems = getMenuItems(nodes);
+ for (IContributionItem menuItem : menuItems) {
+ mMenuManager.insertBefore(endId, menuItem);
+ }
+
+ insertTagSpecificMenus(endId);
+ insertVisualRefactorings(endId);
+ insertParentItems(endId);
+ }
+
+ /**
+ * Returns the list of node-specific actions applicable to the given
+ * collection of nodes
+ *
+ * @param nodes the collection of nodes to look up actions for
+ * @return a list of contribution items applicable for all the nodes
+ */
+ private List<IContributionItem> getMenuItems(List<INode> nodes) {
+ Map<INode, List<RuleAction>> allActions = new HashMap<INode, List<RuleAction>>();
+ for (INode node : nodes) {
+ List<RuleAction> actionList = getMenuActions((NodeProxy) node);
+ allActions.put(node, actionList);
+ }
+
+ Set<String> availableIds = computeApplicableActionIds(allActions);
+
+ // +10: Make room for separators too
+ List<IContributionItem> items = new ArrayList<IContributionItem>(availableIds.size() + 10);
+
+ // We'll use the actions returned by the first node. Even when there
+ // are multiple items selected, we'll use the first action, but pass
+ // the set of all selected nodes to that first action. Actions are required
+ // to work this way to facilitate multi selection and actions which apply
+ // to multiple nodes.
+ NodeProxy first = (NodeProxy) nodes.get(0);
+ List<RuleAction> firstSelectedActions = allActions.get(first);
+ String defaultId = getDefaultActionId(first);
+ for (RuleAction action : firstSelectedActions) {
+ if (!availableIds.contains(action.getId())
+ && !(action instanceof RuleAction.Separator)) {
+ // This action isn't supported by all selected items.
+ continue;
+ }
+
+ items.add(createContributionItem(action, nodes, defaultId));
+ }
+
+ return items;
+ }
+
+ private void insertParentItems(String endId) {
+ List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
+ if (selection.size() == 1) {
+ mMenuManager.insertBefore(endId, new Separator());
+ INode parent = selection.get(0).getNode().getParent();
+ while (parent != null) {
+ String id = parent.getStringAttr(ANDROID_URI, ATTR_ID);
+ String label;
+ if (id != null && id.length() > 0) {
+ label = BaseViewRule.stripIdPrefix(id);
+ } else {
+ // Use the view name, such as "Button", as the label
+ label = parent.getFqcn();
+ // Strip off package
+ label = label.substring(label.lastIndexOf('.') + 1);
+ }
+ mMenuManager.insertBefore(endId, new NestedParentMenu(label, parent));
+ parent = parent.getParent();
+ }
+ mMenuManager.insertBefore(endId, new Separator());
+ }
+ }
+
+ private void insertVisualRefactorings(String endId) {
+ // Extract As <include> refactoring, Wrap In Refactoring, etc.
+ List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
+ if (selection.size() == 0) {
+ return;
+ }
+ // Only include the menu item if you are not right clicking on a root,
+ // or on an included view, or on a non-contiguous selection
+ mMenuManager.insertBefore(endId, new Separator());
+ if (selection.size() == 1 && selection.get(0).getViewInfo() != null
+ && selection.get(0).getViewInfo().getName().equals(FQCN_LINEAR_LAYOUT)) {
+ CanvasViewInfo info = selection.get(0).getViewInfo();
+ List<CanvasViewInfo> children = info.getChildren();
+ if (children.size() == 2) {
+ String first = children.get(0).getName();
+ String second = children.get(1).getName();
+ if ((first.equals(FQCN_IMAGE_VIEW) && second.equals(FQCN_TEXT_VIEW))
+ || (first.equals(FQCN_TEXT_VIEW) && second.equals(FQCN_IMAGE_VIEW))) {
+ mMenuManager.insertBefore(endId, UseCompoundDrawableAction.create(
+ mEditorDelegate));
+ }
+ }
+ }
+ mMenuManager.insertBefore(endId, ExtractIncludeAction.create(mEditorDelegate));
+ mMenuManager.insertBefore(endId, ExtractStyleAction.create(mEditorDelegate));
+ mMenuManager.insertBefore(endId, WrapInAction.create(mEditorDelegate));
+ if (selection.size() == 1 && !(selection.get(0).isRoot())) {
+ mMenuManager.insertBefore(endId, UnwrapAction.create(mEditorDelegate));
+ }
+ if (selection.size() == 1 && (selection.get(0).isLayout() ||
+ selection.get(0).getViewInfo().getName().equals(FQCN_GESTURE_OVERLAY_VIEW))) {
+ mMenuManager.insertBefore(endId, ChangeLayoutAction.create(mEditorDelegate));
+ } else {
+ mMenuManager.insertBefore(endId, ChangeViewAction.create(mEditorDelegate));
+ }
+ mMenuManager.insertBefore(endId, new Separator());
+ }
+
+ /** "Preview List Content" pull-right menu for lists, "Preview Fragment" for fragments, etc. */
+ private void insertTagSpecificMenus(String endId) {
+
+ List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
+ if (selection.size() == 0) {
+ return;
+ }
+ for (SelectionItem item : selection) {
+ UiViewElementNode node = item.getViewInfo().getUiViewNode();
+ String name = node.getDescriptor().getXmlLocalName();
+ boolean isGrid = name.equals(GRID_VIEW);
+ boolean isSpinner = name.equals(SPINNER);
+ if (name.equals(LIST_VIEW) || name.equals(EXPANDABLE_LIST_VIEW)
+ || isGrid || isSpinner) {
+ mMenuManager.insertBefore(endId, new Separator());
+ mMenuManager.insertBefore(endId, new ListViewTypeMenu(mCanvas, isGrid, isSpinner));
+ return;
+ } else if (name.equals(VIEW_FRAGMENT) && selection.size() == 1) {
+ mMenuManager.insertBefore(endId, new Separator());
+ mMenuManager.insertBefore(endId, new FragmentMenu(mCanvas));
+ return;
+ }
+ }
+ }
+
+ /**
+ * Given a map from selection items to list of applicable actions (produced
+ * by {@link #computeApplicableActions()}) this method computes the set of
+ * common actions and returns the action ids of these actions.
+ *
+ * @param actions a map from selection item to list of actions applicable to
+ * that selection item
+ * @return set of action ids for the actions that are present in the action
+ * lists for all selected items
+ */
+ private Set<String> computeApplicableActionIds(Map<INode, List<RuleAction>> actions) {
+ if (actions.size() > 1) {
+ // More than one view is selected, so we have to filter down the available
+ // actions such that only those actions that are defined for all the views
+ // are shown
+ Map<String, Integer> idCounts = new HashMap<String, Integer>();
+ for (Map.Entry<INode, List<RuleAction>> entry : actions.entrySet()) {
+ List<RuleAction> actionList = entry.getValue();
+ for (RuleAction action : actionList) {
+ if (!action.supportsMultipleNodes()) {
+ continue;
+ }
+ String id = action.getId();
+ if (id != null) {
+ assert id != null : action;
+ Integer count = idCounts.get(id);
+ if (count == null) {
+ idCounts.put(id, Integer.valueOf(1));
+ } else {
+ idCounts.put(id, count + 1);
+ }
+ }
+ }
+ }
+ Integer selectionCount = Integer.valueOf(actions.size());
+ Set<String> validIds = new HashSet<String>(idCounts.size());
+ for (Map.Entry<String, Integer> entry : idCounts.entrySet()) {
+ Integer count = entry.getValue();
+ if (selectionCount.equals(count)) {
+ String id = entry.getKey();
+ validIds.add(id);
+ }
+ }
+ return validIds;
+ } else {
+ List<RuleAction> actionList = actions.values().iterator().next();
+ Set<String> validIds = new HashSet<String>(actionList.size());
+ for (RuleAction action : actionList) {
+ String id = action.getId();
+ validIds.add(id);
+ }
+ return validIds;
+ }
+ }
+
+ /**
+ * Returns the menu actions computed by the rule associated with this node.
+ *
+ * @param node the canvas node we need menu actions for
+ * @return a list of {@link RuleAction} objects applicable to the node
+ */
+ private List<RuleAction> getMenuActions(NodeProxy node) {
+ List<RuleAction> actions = mCanvas.getRulesEngine().callGetContextMenu(node);
+ if (actions == null || actions.size() == 0) {
+ return null;
+ }
+
+ return actions;
+ }
+
+ /**
+ * Returns the default action id, or null
+ *
+ * @param node the node to look up the default action for
+ * @return the action id, or null
+ */
+ private String getDefaultActionId(NodeProxy node) {
+ return mCanvas.getRulesEngine().callGetDefaultActionId(node);
+ }
+
+ /**
+ * Creates a {@link ContributionItem} for the given {@link RuleAction}.
+ *
+ * @param action the action to create a {@link ContributionItem} for
+ * @param nodes the set of nodes the action should be applied to
+ * @param defaultId if not non null, the id of an action which should be considered default
+ * @return a new {@link ContributionItem} which implements the given action
+ * on the given nodes
+ */
+ private ContributionItem createContributionItem(final RuleAction action,
+ final List<INode> nodes, final String defaultId) {
+ if (action instanceof RuleAction.Separator) {
+ return new Separator();
+ } else if (action instanceof NestedAction) {
+ NestedAction parentAction = (NestedAction) action;
+ return new ActionContributionItem(new NestedActionMenu(parentAction, nodes));
+ } else if (action instanceof Choices) {
+ Choices parentAction = (Choices) action;
+ return new ActionContributionItem(new NestedChoiceMenu(parentAction, nodes));
+ } else if (action instanceof Toggle) {
+ return new ActionContributionItem(createToggleAction(action, nodes));
+ } else {
+ return new ActionContributionItem(createPlainAction(action, nodes, defaultId));
+ }
+ }
+
+ private Action createToggleAction(final RuleAction action, final List<INode> nodes) {
+ Toggle toggleAction = (Toggle) action;
+ final boolean isChecked = toggleAction.isChecked();
+ Action a = new Action(action.getTitle(), IAction.AS_CHECK_BOX) {
+ @Override
+ public void run() {
+ String label = createActionLabel(action, nodes);
+ mEditorDelegate.getEditor().wrapUndoEditXmlModel(label, new Runnable() {
+ @Override
+ public void run() {
+ action.getCallback().action(action, nodes,
+ null/* no valueId for a toggle */, !isChecked);
+ applyPendingChanges();
+ }
+ });
+ }
+ };
+ a.setId(action.getId());
+ a.setChecked(isChecked);
+ return a;
+ }
+
+ private IAction createPlainAction(final RuleAction action, final List<INode> nodes,
+ final String defaultId) {
+ IAction a = new Action(action.getTitle(), IAction.AS_PUSH_BUTTON) {
+ @Override
+ public void run() {
+ String label = createActionLabel(action, nodes);
+ mEditorDelegate.getEditor().wrapUndoEditXmlModel(label, new Runnable() {
+ @Override
+ public void run() {
+ action.getCallback().action(action, nodes, null,
+ Boolean.TRUE);
+ applyPendingChanges();
+ }
+ });
+ }
+ };
+
+ String id = action.getId();
+ if (defaultId != null && id.equals(defaultId)) {
+ a.setAccelerator(DEFAULT_ACTION_KEY);
+ String text = a.getText();
+ text = text + '\t' + DEFAULT_ACTION_SHORTCUT;
+ a.setText(text);
+
+ } else if (ATTR_ID.equals(id)) {
+ // Keep in sync with {@link LayoutCanvas#handleKeyPressed}
+ if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
+ a.setAccelerator('R' | SWT.MOD1 | SWT.MOD3);
+ // Option+Command
+ a.setText(a.getText().trim() + "\t\u2325\u2318R"); //$NON-NLS-1$
+ } else if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX) {
+ a.setAccelerator('R' | SWT.MOD2 | SWT.MOD3);
+ a.setText(a.getText() + "\tShift+Alt+R"); //$NON-NLS-1$
+ } else {
+ a.setAccelerator('R' | SWT.MOD2 | SWT.MOD3);
+ a.setText(a.getText() + "\tAlt+Shift+R"); //$NON-NLS-1$
+ }
+ }
+ a.setId(id);
+ return a;
+ }
+
+ private static String createActionLabel(final RuleAction action, final List<INode> nodes) {
+ String label = action.getTitle();
+ if (nodes.size() > 1) {
+ label += String.format(" (%d elements)", nodes.size());
+ }
+ return label;
+ }
+
+ /**
+ * The {@link NestedParentMenu} provides submenu content which adds actions
+ * available on one of the selected node's parent nodes. This will be
+ * similar to the menu content for the selected node, except the parent
+ * menus will not be embedded within the nested menu.
+ */
+ private class NestedParentMenu extends SubmenuAction {
+ INode mParent;
+
+ NestedParentMenu(String title, INode parent) {
+ super(title);
+ mParent = parent;
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
+ if (selection.size() == 0) {
+ return;
+ }
+
+ List<IContributionItem> menuItems = getMenuItems(Collections.singletonList(mParent));
+ for (IContributionItem menuItem : menuItems) {
+ menuItem.fill(menu, -1);
+ }
+ }
+ }
+
+ /**
+ * The {@link NestedActionMenu} creates a lazily populated pull-right menu
+ * where the children are {@link RuleAction}'s themselves.
+ */
+ private class NestedActionMenu extends SubmenuAction {
+ private final NestedAction mParentAction;
+ private final List<INode> mNodes;
+
+ NestedActionMenu(NestedAction parentAction, List<INode> nodes) {
+ super(parentAction.getTitle());
+ mParentAction = parentAction;
+ mNodes = nodes;
+
+ assert mNodes.size() > 0;
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ Map<INode, List<RuleAction>> allActions = new HashMap<INode, List<RuleAction>>();
+ for (INode node : mNodes) {
+ List<RuleAction> actionList = mParentAction.getNestedActions(node);
+ allActions.put(node, actionList);
+ }
+
+ Set<String> availableIds = computeApplicableActionIds(allActions);
+
+ NodeProxy first = (NodeProxy) mNodes.get(0);
+ String defaultId = getDefaultActionId(first);
+ List<RuleAction> firstSelectedActions = allActions.get(first);
+
+ int count = 0;
+ for (RuleAction firstAction : firstSelectedActions) {
+ if (!availableIds.contains(firstAction.getId())
+ && !(firstAction instanceof RuleAction.Separator)) {
+ // This action isn't supported by all selected items.
+ continue;
+ }
+
+ createContributionItem(firstAction, mNodes, defaultId).fill(menu, -1);
+ count++;
+ }
+
+ if (count == 0) {
+ addDisabledMessageItem("<Empty>");
+ }
+ }
+ }
+
+ private void applyPendingChanges() {
+ LayoutCanvas canvas = mEditorDelegate.getGraphicalEditor().getCanvasControl();
+ CanvasViewInfo root = canvas.getViewHierarchy().getRoot();
+ if (root != null) {
+ UiViewElementNode uiViewNode = root.getUiViewNode();
+ NodeFactory nodeFactory = canvas.getNodeFactory();
+ NodeProxy rootNode = nodeFactory.create(uiViewNode);
+ if (rootNode != null) {
+ rootNode.applyPendingChanges();
+ }
+ }
+ }
+
+ /**
+ * The {@link NestedChoiceMenu} creates a lazily populated pull-right menu
+ * where the items in the menu are strings
+ */
+ private class NestedChoiceMenu extends SubmenuAction {
+ private final Choices mParentAction;
+ private final List<INode> mNodes;
+
+ NestedChoiceMenu(Choices parentAction, List<INode> nodes) {
+ super(parentAction.getTitle());
+ mParentAction = parentAction;
+ mNodes = nodes;
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ List<String> titles = mParentAction.getTitles();
+ List<String> ids = mParentAction.getIds();
+ String current = mParentAction.getCurrent();
+ assert titles.size() == ids.size();
+ String[] currentValues = current != null
+ && current.indexOf(RuleAction.CHOICE_SEP) != -1 ?
+ current.split(RuleAction.CHOICE_SEP_PATTERN) : null;
+ for (int i = 0, n = Math.min(titles.size(), ids.size()); i < n; i++) {
+ final String id = ids.get(i);
+ if (id == null || id.equals(RuleAction.SEPARATOR)) {
+ new Separator().fill(menu, -1);
+ continue;
+ }
+
+ // Find out whether this item is selected
+ boolean select = false;
+ if (current != null) {
+ // The current choice has a separator, so it's a flag with
+ // multiple values selected. Compare keys with the split
+ // values.
+ if (currentValues != null) {
+ if (current.indexOf(id) >= 0) {
+ for (String value : currentValues) {
+ if (id.equals(value)) {
+ select = true;
+ break;
+ }
+ }
+ }
+ } else {
+ // current choice has no separator, simply compare to the key
+ select = id.equals(current);
+ }
+ }
+
+ String title = titles.get(i);
+ IAction a = new Action(title,
+ current != null ? IAction.AS_CHECK_BOX : IAction.AS_PUSH_BUTTON) {
+ @Override
+ public void runWithEvent(Event event) {
+ run();
+ }
+ @Override
+ public void run() {
+ String label = createActionLabel(mParentAction, mNodes);
+ mEditorDelegate.getEditor().wrapUndoEditXmlModel(label, new Runnable() {
+ @Override
+ public void run() {
+ mParentAction.getCallback().action(mParentAction, mNodes, id,
+ Boolean.TRUE);
+ applyPendingChanges();
+ }
+ });
+ }
+ };
+ a.setId(id);
+ a.setEnabled(true);
+ if (select) {
+ a.setChecked(true);
+ }
+
+ new ActionContributionItem(a).fill(menu, -1);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/EmptyViewsOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/EmptyViewsOverlay.java
new file mode 100644
index 000000000..daa3e0eae
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/EmptyViewsOverlay.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2010 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 org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+
+/**
+ * The {@link EmptyViewsOverlay} paints bounding rectangles for any of the empty and
+ * invisible container views in the scene.
+ */
+public class EmptyViewsOverlay extends Overlay {
+ /** The {@link ViewHierarchy} containing visible view information. */
+ private final ViewHierarchy mViewHierarchy;
+
+ /** Border color to paint the bounding boxes with. */
+ private Color mBorderColor;
+
+ /** Vertical scaling & scrollbar information. */
+ private CanvasTransform mVScale;
+
+ /** Horizontal scaling & scrollbar information. */
+ private CanvasTransform mHScale;
+
+ /**
+ * Constructs a new {@link EmptyViewsOverlay} linked to the given view hierarchy.
+ *
+ * @param viewHierarchy The {@link ViewHierarchy} to render.
+ * @param hScale The {@link CanvasTransform} to use to transfer horizontal layout
+ * coordinates to screen coordinates.
+ * @param vScale The {@link CanvasTransform} to use to transfer vertical layout coordinates
+ * to screen coordinates.
+ */
+ public EmptyViewsOverlay(
+ ViewHierarchy viewHierarchy,
+ CanvasTransform hScale,
+ CanvasTransform vScale) {
+ super();
+ mViewHierarchy = viewHierarchy;
+ mHScale = hScale;
+ mVScale = vScale;
+ }
+
+ @Override
+ public void create(Device device) {
+ mBorderColor = new Color(device, SwtDrawingStyle.EMPTY.getStrokeColor());
+ }
+
+ @Override
+ public void dispose() {
+ if (mBorderColor != null) {
+ mBorderColor.dispose();
+ mBorderColor = null;
+ }
+ }
+
+ @Override
+ public void paint(GC gc) {
+ gc.setForeground(mBorderColor);
+ gc.setLineDash(null);
+ gc.setLineStyle(SwtDrawingStyle.EMPTY.getLineStyle());
+ int oldAlpha = gc.getAlpha();
+ gc.setAlpha(SwtDrawingStyle.EMPTY.getStrokeAlpha());
+ gc.setLineWidth(SwtDrawingStyle.EMPTY.getLineWidth());
+
+ for (CanvasViewInfo info : mViewHierarchy.getInvisibleViews()) {
+ Rectangle r = info.getAbsRect();
+
+ int x = mHScale.translate(r.x);
+ int y = mVScale.translate(r.y);
+ int w = mHScale.scale(r.width);
+ int h = mVScale.scale(r.height);
+
+ // +1: See explanation in equivalent code in {@link OutlineOverlay#paint}
+ gc.drawRectangle(x, y, w + 1, h + 1);
+ }
+
+ gc.setAlpha(oldAlpha);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExportScreenshotAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExportScreenshotAction.java
new file mode 100644
index 000000000..ac3328db2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExportScreenshotAction.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2012 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 static com.android.SdkConstants.DOT_PNG;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Shell;
+
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+
+import javax.imageio.ImageIO;
+
+/** Saves the current layout editor's rendered image to disk */
+class ExportScreenshotAction extends Action {
+ private final LayoutCanvas mCanvas;
+
+ ExportScreenshotAction(LayoutCanvas canvas) {
+ super("Export Screenshot...");
+ mCanvas = canvas;
+ }
+
+ @Override
+ public void run() {
+ Shell shell = AdtPlugin.getShell();
+
+ ImageOverlay imageOverlay = mCanvas.getImageOverlay();
+ BufferedImage image = imageOverlay.getAwtImage();
+ if (image != null) {
+ FileDialog dialog = new FileDialog(shell, SWT.SAVE);
+ dialog.setFilterExtensions(new String[] { "*.png" }); //$NON-NLS-1$
+ String path = dialog.open();
+ if (path != null) {
+ if (!path.endsWith(DOT_PNG)) {
+ path = path + DOT_PNG;
+ }
+ File file = new File(path);
+ if (file.exists()) {
+ MessageDialog d = new MessageDialog(null, "File Already Exists", null,
+ String.format(
+ "%1$s already exists.\nWould you like to replace it?",
+ path),
+ MessageDialog.QUESTION, new String[] {
+ // Yes will be moved to the end because it's the default
+ "Yes", "No"
+ }, 0);
+ int result = d.open();
+ if (result != 0) {
+ return;
+ }
+ }
+ try {
+ ImageIO.write(image, "PNG", file); //$NON-NLS-1$
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ } else {
+ MessageDialog.openError(shell, "Error", "Image not available");
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/FragmentMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/FragmentMenu.java
new file mode 100644
index 000000000..f7085fc12
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/FragmentMenu.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2011 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 static com.android.SdkConstants.ANDROID_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_CLASS;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata.KEY_FRAGMENT_LAYOUT;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.resources.CyclicDependencyValidator;
+import com.android.ide.eclipse.adt.internal.ui.ResourceChooser;
+import com.android.resources.ResourceType;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.widgets.Menu;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Fragment context menu allowing a layout to be chosen for previewing in the fragment frame.
+ */
+public class FragmentMenu extends SubmenuAction {
+ private static final String R_LAYOUT_RESOURCE_PREFIX = "R.layout."; //$NON-NLS-1$
+ private static final String ANDROID_R_PREFIX = "android.R.layout"; //$NON-NLS-1$
+
+ /** Associated canvas */
+ private final LayoutCanvas mCanvas;
+
+ /**
+ * Creates a "Preview Fragment" menu
+ *
+ * @param canvas associated canvas
+ */
+ public FragmentMenu(LayoutCanvas canvas) {
+ super("Fragment Layout");
+ mCanvas = canvas;
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ IAction action = new PickLayoutAction("Choose Layout...");
+ new ActionContributionItem(action).fill(menu, -1);
+
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+ List<SelectionItem> selections = selectionManager.getSelections();
+ if (selections.size() == 0) {
+ return;
+ }
+
+ SelectionItem first = selections.get(0);
+ UiViewElementNode node = first.getViewInfo().getUiViewNode();
+ if (node == null) {
+ return;
+ }
+ Element element = (Element) node.getXmlNode();
+
+ String selected = getSelectedLayout();
+ if (selected != null) {
+ if (selected.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX)) {
+ selected = selected.substring(ANDROID_LAYOUT_RESOURCE_PREFIX.length());
+ }
+ }
+
+ String fqcn = getFragmentClass(element);
+ if (fqcn != null) {
+ // Look up the corresponding activity class and try to figure out
+ // which layouts it is referring to and list these here as reasonable
+ // guesses
+ IProject project = mCanvas.getEditorDelegate().getEditor().getProject();
+ String source = null;
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ IType type = javaProject.findType(fqcn);
+ if (type != null) {
+ source = type.getSource();
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ // Find layouts. This is based on just skimming the Fragment class and looking
+ // for layout references of the form R.layout.*.
+ if (source != null) {
+ String self = mCanvas.getLayoutResourceName();
+ // Pair of <title,layout> to be displayed to the user
+ List<Pair<String, String>> layouts = new ArrayList<Pair<String, String>>();
+
+ if (source.contains("extends ListFragment")) { //$NON-NLS-1$
+ layouts.add(Pair.of("list_content", //$NON-NLS-1$
+ "@android:layout/list_content")); //$NON-NLS-1$
+ }
+
+ int index = 0;
+ while (true) {
+ index = source.indexOf(R_LAYOUT_RESOURCE_PREFIX, index);
+ if (index == -1) {
+ break;
+ } else {
+ index += R_LAYOUT_RESOURCE_PREFIX.length();
+ int end = index;
+ while (end < source.length()) {
+ char c = source.charAt(end);
+ if (!Character.isJavaIdentifierPart(c)) {
+ break;
+ }
+ end++;
+ }
+ if (end > index) {
+ String title = source.substring(index, end);
+ String layout;
+ // Is this R.layout part of an android.R.layout?
+ int len = ANDROID_R_PREFIX.length() + 1; // prefix length to check
+ if (index > len && source.startsWith(ANDROID_R_PREFIX, index - len)) {
+ layout = ANDROID_LAYOUT_RESOURCE_PREFIX + title;
+ } else {
+ layout = LAYOUT_RESOURCE_PREFIX + title;
+ }
+ if (!self.equals(title)) {
+ layouts.add(Pair.of(title, layout));
+ }
+ }
+ }
+
+ index++;
+ }
+
+ if (layouts.size() > 0) {
+ new Separator().fill(menu, -1);
+ for (Pair<String, String> layout : layouts) {
+ action = new SetFragmentLayoutAction(layout.getFirst(),
+ layout.getSecond(), selected);
+ new ActionContributionItem(action).fill(menu, -1);
+ }
+ }
+ }
+ }
+
+ if (selected != null) {
+ new Separator().fill(menu, -1);
+ action = new SetFragmentLayoutAction("Clear", null, null);
+ new ActionContributionItem(action).fill(menu, -1);
+ }
+ }
+
+ /**
+ * Returns the class name of the fragment associated with the given {@code <fragment>}
+ * element.
+ *
+ * @param element the element for the fragment tag
+ * @return the fully qualified fragment class name, or null
+ */
+ @Nullable
+ public static String getFragmentClass(@NonNull Element element) {
+ String fqcn = element.getAttribute(ATTR_CLASS);
+ if (fqcn == null || fqcn.length() == 0) {
+ fqcn = element.getAttributeNS(ANDROID_URI, ATTR_NAME);
+ }
+ if (fqcn != null && fqcn.length() > 0) {
+ return fqcn;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the layout to be shown for the given {@code <fragment>} node.
+ *
+ * @param node the node corresponding to the {@code <fragment>} element
+ * @return the resource path to a layout to render for this fragment, or null
+ */
+ @Nullable
+ public static String getFragmentLayout(@NonNull Node node) {
+ String layout = LayoutMetadata.getProperty(
+ node, LayoutMetadata.KEY_FRAGMENT_LAYOUT);
+ if (layout != null) {
+ return layout;
+ }
+
+ return null;
+ }
+
+ /** Returns the name of the currently displayed layout in the fragment, or null */
+ @Nullable
+ private String getSelectedLayout() {
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+ for (SelectionItem item : selectionManager.getSelections()) {
+ UiViewElementNode node = item.getViewInfo().getUiViewNode();
+ if (node != null) {
+ String layout = getFragmentLayout(node.getXmlNode());
+ if (layout != null) {
+ return layout;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Set the given layout as the new fragment layout
+ *
+ * @param layout the layout resource name to show in this fragment
+ */
+ public void setNewLayout(@Nullable String layout) {
+ LayoutEditorDelegate delegate = mCanvas.getEditorDelegate();
+ GraphicalEditorPart graphicalEditor = delegate.getGraphicalEditor();
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+
+ for (SelectionItem item : selectionManager.getSnapshot()) {
+ UiViewElementNode node = item.getViewInfo().getUiViewNode();
+ if (node != null) {
+ Node xmlNode = node.getXmlNode();
+ LayoutMetadata.setProperty(delegate.getEditor(), xmlNode, KEY_FRAGMENT_LAYOUT,
+ layout);
+ }
+ }
+
+ // Refresh
+ graphicalEditor.recomputeLayout();
+ mCanvas.redraw();
+ }
+
+ /** Action to set the given layout as the new layout in a fragment */
+ private class SetFragmentLayoutAction extends Action {
+ private final String mLayout;
+
+ public SetFragmentLayoutAction(String title, String layout, String selected) {
+ super(title, IAction.AS_RADIO_BUTTON);
+ mLayout = layout;
+
+ if (layout != null && layout.equals(selected)) {
+ setChecked(true);
+ }
+ }
+
+ @Override
+ public void run() {
+ if (isChecked()) {
+ setNewLayout(mLayout);
+ }
+ }
+ }
+
+ /**
+ * Action which brings up the "Create new XML File" wizard, pre-selected with the
+ * animation category
+ */
+ private class PickLayoutAction extends Action {
+
+ public PickLayoutAction(String title) {
+ super(title, IAction.AS_PUSH_BUTTON);
+ }
+
+ @Override
+ public void run() {
+ LayoutEditorDelegate delegate = mCanvas.getEditorDelegate();
+ IFile file = delegate.getEditor().getInputFile();
+ GraphicalEditorPart editor = delegate.getGraphicalEditor();
+ ResourceChooser dlg = ResourceChooser.create(editor, ResourceType.LAYOUT)
+ .setInputValidator(CyclicDependencyValidator.create(file))
+ .setInitialSize(85, 10)
+ .setCurrentResource(getSelectedLayout());
+ int result = dlg.open();
+ if (result == ResourceChooser.CLEAR_RETURN_CODE) {
+ setNewLayout(null);
+ } else if (result == Window.OK) {
+ String newType = dlg.getCurrentResource();
+ setNewLayout(newType);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java
new file mode 100644
index 000000000..354517e76
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java
@@ -0,0 +1,645 @@
+/*
+ * Copyright (C) 2010 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.annotations.NonNull;
+import com.android.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.IColor;
+import com.android.ide.common.api.IGraphics;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.Point;
+import com.android.ide.common.api.Rect;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.FontMetrics;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.RGB;
+
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Wraps an SWT {@link GC} into an {@link IGraphics} interface so that {@link IViewRule} objects
+ * can directly draw on the canvas.
+ * <p/>
+ * The actual wrapped GC object is only non-null during the context of a paint operation.
+ */
+public class GCWrapper implements IGraphics {
+
+ /**
+ * The actual SWT {@link GC} being wrapped. This can change during the lifetime of the
+ * object. It is generally set to something during an onPaint method and then changed
+ * to null when not in the context of a paint.
+ */
+ private GC mGc;
+
+ /**
+ * Current style being used for drawing.
+ */
+ private SwtDrawingStyle mCurrentStyle = SwtDrawingStyle.INVALID;
+
+ /**
+ * Implementation of IColor wrapping an SWT color.
+ */
+ private static class ColorWrapper implements IColor {
+ private final Color mColor;
+
+ public ColorWrapper(Color color) {
+ mColor = color;
+ }
+
+ public Color getColor() {
+ return mColor;
+ }
+ }
+
+ /** A map of registered colors. All these colors must be disposed at the end. */
+ private final HashMap<Integer, ColorWrapper> mColorMap = new HashMap<Integer, ColorWrapper>();
+
+ /**
+ * A map of the {@link SwtDrawingStyle} stroke colors that we have actually
+ * used (to be disposed)
+ */
+ private final Map<DrawingStyle, Color> mStyleStrokeMap = new EnumMap<DrawingStyle, Color>(
+ DrawingStyle.class);
+
+ /**
+ * A map of the {@link SwtDrawingStyle} fill colors that we have actually
+ * used (to be disposed)
+ */
+ private final Map<DrawingStyle, Color> mStyleFillMap = new EnumMap<DrawingStyle, Color>(
+ DrawingStyle.class);
+
+ /** The cached pixel height of the default current font. */
+ private int mFontHeight = 0;
+
+ /** The scaling of the canvas in X. */
+ private final CanvasTransform mHScale;
+ /** The scaling of the canvas in Y. */
+ private final CanvasTransform mVScale;
+
+ public GCWrapper(CanvasTransform hScale, CanvasTransform vScale) {
+ mHScale = hScale;
+ mVScale = vScale;
+ mGc = null;
+ }
+
+ void setGC(GC gc) {
+ mGc = gc;
+ }
+
+ private GC getGc() {
+ return mGc;
+ }
+
+ void checkGC() {
+ if (mGc == null) {
+ throw new RuntimeException("IGraphics used without a valid context.");
+ }
+ }
+
+ void dispose() {
+ for (ColorWrapper c : mColorMap.values()) {
+ c.getColor().dispose();
+ }
+ mColorMap.clear();
+
+ for (Color c : mStyleStrokeMap.values()) {
+ c.dispose();
+ }
+ mStyleStrokeMap.clear();
+
+ for (Color c : mStyleFillMap.values()) {
+ c.dispose();
+ }
+ mStyleFillMap.clear();
+ }
+
+ //-------------
+
+ @Override
+ public @NonNull IColor registerColor(int rgb) {
+ checkGC();
+
+ Integer key = Integer.valueOf(rgb);
+ ColorWrapper c = mColorMap.get(key);
+ if (c == null) {
+ c = new ColorWrapper(new Color(getGc().getDevice(),
+ (rgb >> 16) & 0xFF,
+ (rgb >> 8) & 0xFF,
+ (rgb >> 0) & 0xFF));
+ mColorMap.put(key, c);
+ }
+
+ return c;
+ }
+
+ /** Returns the (cached) pixel height of the current font. */
+ @Override
+ public int getFontHeight() {
+ if (mFontHeight < 1) {
+ checkGC();
+ FontMetrics fm = getGc().getFontMetrics();
+ mFontHeight = fm.getHeight();
+ }
+ return mFontHeight;
+ }
+
+ @Override
+ public @NonNull IColor getForeground() {
+ Color c = getGc().getForeground();
+ return new ColorWrapper(c);
+ }
+
+ @Override
+ public @NonNull IColor getBackground() {
+ Color c = getGc().getBackground();
+ return new ColorWrapper(c);
+ }
+
+ @Override
+ public int getAlpha() {
+ return getGc().getAlpha();
+ }
+
+ @Override
+ public void setForeground(@NonNull IColor color) {
+ checkGC();
+ getGc().setForeground(((ColorWrapper) color).getColor());
+ }
+
+ @Override
+ public void setBackground(@NonNull IColor color) {
+ checkGC();
+ getGc().setBackground(((ColorWrapper) color).getColor());
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ checkGC();
+ try {
+ getGc().setAlpha(alpha);
+ } catch (SWTException e) {
+ // This means that we cannot set the alpha on this platform; this is
+ // an acceptable no-op.
+ }
+ }
+
+ @Override
+ public void setLineStyle(@NonNull LineStyle style) {
+ int swtStyle = 0;
+ switch (style) {
+ case LINE_SOLID:
+ swtStyle = SWT.LINE_SOLID;
+ break;
+ case LINE_DASH:
+ swtStyle = SWT.LINE_DASH;
+ break;
+ case LINE_DOT:
+ swtStyle = SWT.LINE_DOT;
+ break;
+ case LINE_DASHDOT:
+ swtStyle = SWT.LINE_DASHDOT;
+ break;
+ case LINE_DASHDOTDOT:
+ swtStyle = SWT.LINE_DASHDOTDOT;
+ break;
+ default:
+ assert false : style;
+ break;
+ }
+
+ if (swtStyle != 0) {
+ checkGC();
+ getGc().setLineStyle(swtStyle);
+ }
+ }
+
+ @Override
+ public void setLineWidth(int width) {
+ checkGC();
+ if (width > 0) {
+ getGc().setLineWidth(width);
+ }
+ }
+
+ // lines
+
+ @Override
+ public void drawLine(int x1, int y1, int x2, int y2) {
+ checkGC();
+ useStrokeAlpha();
+ x1 = mHScale.translate(x1);
+ y1 = mVScale.translate(y1);
+ x2 = mHScale.translate(x2);
+ y2 = mVScale.translate(y2);
+ getGc().drawLine(x1, y1, x2, y2);
+ }
+
+ @Override
+ public void drawLine(@NonNull Point p1, @NonNull Point p2) {
+ drawLine(p1.x, p1.y, p2.x, p2.y);
+ }
+
+ // rectangles
+
+ @Override
+ public void drawRect(int x1, int y1, int x2, int y2) {
+ checkGC();
+ useStrokeAlpha();
+ int x = mHScale.translate(x1);
+ int y = mVScale.translate(y1);
+ int w = mHScale.scale(x2 - x1);
+ int h = mVScale.scale(y2 - y1);
+ getGc().drawRectangle(x, y, w, h);
+ }
+
+ @Override
+ public void drawRect(@NonNull Point p1, @NonNull Point p2) {
+ drawRect(p1.x, p1.y, p2.x, p2.y);
+ }
+
+ @Override
+ public void drawRect(@NonNull Rect r) {
+ checkGC();
+ useStrokeAlpha();
+ int x = mHScale.translate(r.x);
+ int y = mVScale.translate(r.y);
+ int w = mHScale.scale(r.w);
+ int h = mVScale.scale(r.h);
+ getGc().drawRectangle(x, y, w, h);
+ }
+
+ @Override
+ public void fillRect(int x1, int y1, int x2, int y2) {
+ checkGC();
+ useFillAlpha();
+ int x = mHScale.translate(x1);
+ int y = mVScale.translate(y1);
+ int w = mHScale.scale(x2 - x1);
+ int h = mVScale.scale(y2 - y1);
+ getGc().fillRectangle(x, y, w, h);
+ }
+
+ @Override
+ public void fillRect(@NonNull Point p1, @NonNull Point p2) {
+ fillRect(p1.x, p1.y, p2.x, p2.y);
+ }
+
+ @Override
+ public void fillRect(@NonNull Rect r) {
+ checkGC();
+ useFillAlpha();
+ int x = mHScale.translate(r.x);
+ int y = mVScale.translate(r.y);
+ int w = mHScale.scale(r.w);
+ int h = mVScale.scale(r.h);
+ getGc().fillRectangle(x, y, w, h);
+ }
+
+ // circles (actually ovals)
+
+ public void drawOval(int x1, int y1, int x2, int y2) {
+ checkGC();
+ useStrokeAlpha();
+ int x = mHScale.translate(x1);
+ int y = mVScale.translate(y1);
+ int w = mHScale.scale(x2 - x1);
+ int h = mVScale.scale(y2 - y1);
+ getGc().drawOval(x, y, w, h);
+ }
+
+ public void drawOval(Point p1, Point p2) {
+ drawOval(p1.x, p1.y, p2.x, p2.y);
+ }
+
+ public void drawOval(Rect r) {
+ checkGC();
+ useStrokeAlpha();
+ int x = mHScale.translate(r.x);
+ int y = mVScale.translate(r.y);
+ int w = mHScale.scale(r.w);
+ int h = mVScale.scale(r.h);
+ getGc().drawOval(x, y, w, h);
+ }
+
+ public void fillOval(int x1, int y1, int x2, int y2) {
+ checkGC();
+ useFillAlpha();
+ int x = mHScale.translate(x1);
+ int y = mVScale.translate(y1);
+ int w = mHScale.scale(x2 - x1);
+ int h = mVScale.scale(y2 - y1);
+ getGc().fillOval(x, y, w, h);
+ }
+
+ public void fillOval(Point p1, Point p2) {
+ fillOval(p1.x, p1.y, p2.x, p2.y);
+ }
+
+ public void fillOval(Rect r) {
+ checkGC();
+ useFillAlpha();
+ int x = mHScale.translate(r.x);
+ int y = mVScale.translate(r.y);
+ int w = mHScale.scale(r.w);
+ int h = mVScale.scale(r.h);
+ getGc().fillOval(x, y, w, h);
+ }
+
+
+ // strings
+
+ @Override
+ public void drawString(@NonNull String string, int x, int y) {
+ checkGC();
+ useStrokeAlpha();
+ x = mHScale.translate(x);
+ y = mVScale.translate(y);
+ // Background fill of text is not useful because it does not
+ // use the alpha; we instead supply a separate method (drawBoxedStrings) which
+ // first paints a semi-transparent mask for the text to sit on
+ // top of (this ensures that the text is readable regardless of
+ // colors of the pixels below the text)
+ getGc().drawString(string, x, y, true /*isTransparent*/);
+ }
+
+ @Override
+ public void drawBoxedStrings(int x, int y, @NonNull List<?> strings) {
+ checkGC();
+
+ x = mHScale.translate(x);
+ y = mVScale.translate(y);
+
+ // Compute bounds of the box by adding up the sum of the text heights
+ // and the max of the text widths
+ int width = 0;
+ int height = 0;
+ int lineHeight = getGc().getFontMetrics().getHeight();
+ for (Object s : strings) {
+ org.eclipse.swt.graphics.Point extent = getGc().stringExtent(s.toString());
+ height += extent.y;
+ width = Math.max(width, extent.x);
+ }
+
+ // Paint a box below the text
+ int padding = 2;
+ useFillAlpha();
+ getGc().fillRectangle(x - padding, y - padding, width + 2 * padding, height + 2 * padding);
+
+ // Finally draw strings on top
+ useStrokeAlpha();
+ int lineY = y;
+ for (Object s : strings) {
+ getGc().drawString(s.toString(), x, lineY, true /* isTransparent */);
+ lineY += lineHeight;
+ }
+ }
+
+ @Override
+ public void drawString(@NonNull String string, @NonNull Point topLeft) {
+ drawString(string, topLeft.x, topLeft.y);
+ }
+
+ // Styles
+
+ @Override
+ public void useStyle(@NonNull DrawingStyle style) {
+ checkGC();
+
+ // Look up the specific SWT style which defines the actual
+ // colors and attributes to be used for the logical drawing style.
+ SwtDrawingStyle swtStyle = SwtDrawingStyle.of(style);
+ RGB stroke = swtStyle.getStrokeColor();
+ if (stroke != null) {
+ Color color = getStrokeColor(style, stroke);
+ mGc.setForeground(color);
+ }
+ RGB fill = swtStyle.getFillColor();
+ if (fill != null) {
+ Color color = getFillColor(style, fill);
+ mGc.setBackground(color);
+ }
+ mGc.setLineWidth(swtStyle.getLineWidth());
+ mGc.setLineStyle(swtStyle.getLineStyle());
+ if (swtStyle.getLineStyle() == SWT.LINE_CUSTOM) {
+ mGc.setLineDash(new int[] {
+ 8, 4
+ });
+ }
+ mCurrentStyle = swtStyle;
+ }
+
+ /** Uses the stroke alpha for subsequent drawing operations. */
+ private void useStrokeAlpha() {
+ mGc.setAlpha(mCurrentStyle.getStrokeAlpha());
+ }
+
+ /** Uses the fill alpha for subsequent drawing operations. */
+ private void useFillAlpha() {
+ mGc.setAlpha(mCurrentStyle.getFillAlpha());
+ }
+
+ /**
+ * Get the SWT stroke color (foreground/border) to use for the given style,
+ * using the provided color description if we haven't seen this color yet.
+ * The color will also be placed in the {@link #mStyleStrokeMap} such that
+ * it can be disposed of at cleanup time.
+ *
+ * @param style The drawing style for which we want a color
+ * @param defaultColorDesc The RGB values to initialize the color to if we
+ * haven't seen this color before
+ * @return The color object
+ */
+ private Color getStrokeColor(DrawingStyle style, RGB defaultColorDesc) {
+ return getStyleColor(style, defaultColorDesc, mStyleStrokeMap);
+ }
+
+ /**
+ * Get the SWT fill (background/interior) color to use for the given style,
+ * using the provided color description if we haven't seen this color yet.
+ * The color will also be placed in the {@link #mStyleStrokeMap} such that
+ * it can be disposed of at cleanup time.
+ *
+ * @param style The drawing style for which we want a color
+ * @param defaultColorDesc The RGB values to initialize the color to if we
+ * haven't seen this color before
+ * @return The color object
+ */
+ private Color getFillColor(DrawingStyle style, RGB defaultColorDesc) {
+ return getStyleColor(style, defaultColorDesc, mStyleFillMap);
+ }
+
+ /**
+ * Get the SWT color to use for the given style, using the provided color
+ * description if we haven't seen this color yet. The color will also be
+ * placed in the map referenced by the map parameter such that it can be
+ * disposed of at cleanup time.
+ *
+ * @param style The drawing style for which we want a color
+ * @param defaultColorDesc The RGB values to initialize the color to if we
+ * haven't seen this color before
+ * @param map The color map to use
+ * @return The color object
+ */
+ private Color getStyleColor(DrawingStyle style, RGB defaultColorDesc,
+ Map<DrawingStyle, Color> map) {
+ Color color = map.get(style);
+ if (color == null) {
+ color = new Color(getGc().getDevice(), defaultColorDesc);
+ map.put(style, color);
+ }
+
+ return color;
+ }
+
+ // dots
+
+ @Override
+ public void drawPoint(int x, int y) {
+ checkGC();
+ useStrokeAlpha();
+ x = mHScale.translate(x);
+ y = mVScale.translate(y);
+
+ getGc().drawPoint(x, y);
+ }
+
+ // arrows
+
+ private static final int MIN_LENGTH = 10;
+
+
+ @Override
+ public void drawArrow(int x1, int y1, int x2, int y2, int size) {
+ int arrowWidth = size;
+ int arrowHeight = size;
+
+ checkGC();
+ useStrokeAlpha();
+ x1 = mHScale.translate(x1);
+ y1 = mVScale.translate(y1);
+ x2 = mHScale.translate(x2);
+ y2 = mVScale.translate(y2);
+ GC graphics = getGc();
+
+ // Make size adjustments to ensure that the arrow has enough width to be visible
+ if (x1 == x2 && Math.abs(y1 - y2) < MIN_LENGTH) {
+ int delta = (MIN_LENGTH - Math.abs(y1 - y2)) / 2;
+ if (y1 < y2) {
+ y1 -= delta;
+ y2 += delta;
+ } else {
+ y1 += delta;
+ y2-= delta;
+ }
+
+ } else if (y1 == y2 && Math.abs(x1 - x2) < MIN_LENGTH) {
+ int delta = (MIN_LENGTH - Math.abs(x1 - x2)) / 2;
+ if (x1 < x2) {
+ x1 -= delta;
+ x2 += delta;
+ } else {
+ x1 += delta;
+ x2-= delta;
+ }
+ }
+
+ graphics.drawLine(x1, y1, x2, y2);
+
+ // Arrowhead:
+
+ if (x1 == x2) {
+ // Vertical
+ if (y2 > y1) {
+ graphics.drawLine(x2 - arrowWidth, y2 - arrowHeight, x2, y2);
+ graphics.drawLine(x2 + arrowWidth, y2 - arrowHeight, x2, y2);
+ } else {
+ graphics.drawLine(x2 - arrowWidth, y2 + arrowHeight, x2, y2);
+ graphics.drawLine(x2 + arrowWidth, y2 + arrowHeight, x2, y2);
+ }
+ } else if (y1 == y2) {
+ // Horizontal
+ if (x2 > x1) {
+ graphics.drawLine(x2 - arrowHeight, y2 - arrowWidth, x2, y2);
+ graphics.drawLine(x2 - arrowHeight, y2 + arrowWidth, x2, y2);
+ } else {
+ graphics.drawLine(x2 + arrowHeight, y2 - arrowWidth, x2, y2);
+ graphics.drawLine(x2 + arrowHeight, y2 + arrowWidth, x2, y2);
+ }
+ } else {
+ // Compute angle:
+ int dy = y2 - y1;
+ int dx = x2 - x1;
+ double angle = Math.atan2(dy, dx);
+ double lineLength = Math.sqrt(dy * dy + dx * dx);
+
+ // Imagine a line of the same length as the arrow, but with angle 0.
+ // Its two arrow lines are at (-arrowWidth, -arrowHeight) relative
+ // to the endpoint (x1 + lineLength, y1) stretching up to (x2,y2).
+ // We compute the positions of (ax,ay) for the point above and
+ // below this line and paint the lines to it:
+ double ax = x1 + lineLength - arrowHeight;
+ double ay = y1 - arrowWidth;
+ int rx = (int) (Math.cos(angle) * (ax-x1) - Math.sin(angle) * (ay-y1) + x1);
+ int ry = (int) (Math.sin(angle) * (ax-x1) + Math.cos(angle) * (ay-y1) + y1);
+ graphics.drawLine(x2, y2, rx, ry);
+
+ ay = y1 + arrowWidth;
+ rx = (int) (Math.cos(angle) * (ax-x1) - Math.sin(angle) * (ay-y1) + x1);
+ ry = (int) (Math.sin(angle) * (ax-x1) + Math.cos(angle) * (ay-y1) + y1);
+ graphics.drawLine(x2, y2, rx, ry);
+ }
+
+ /* TODO: Experiment with filled arrow heads?
+ if (x1 == x2) {
+ // Vertical
+ if (y2 > y1) {
+ for (int i = 0; i < arrowWidth; i++) {
+ graphics.drawLine(x2 - arrowWidth + i, y2 - arrowWidth + i,
+ x2 + arrowWidth - i, y2 - arrowWidth + i);
+ }
+ } else {
+ for (int i = 0; i < arrowWidth; i++) {
+ graphics.drawLine(x2 - arrowWidth + i, y2 + arrowWidth - i,
+ x2 + arrowWidth - i, y2 + arrowWidth - i);
+ }
+ }
+ } else if (y1 == y2) {
+ // Horizontal
+ if (x2 > x1) {
+ for (int i = 0; i < arrowHeight; i++) {
+ graphics.drawLine(x2 - arrowHeight + i, y2 - arrowHeight + i, x2
+ - arrowHeight + i, y2 + arrowHeight - i);
+ }
+ } else {
+ for (int i = 0; i < arrowHeight; i++) {
+ graphics.drawLine(x2 + arrowHeight - i, y2 - arrowHeight + i, x2
+ + arrowHeight - i, y2 + arrowHeight - i);
+ }
+ }
+ } else {
+ // Arbitrary angle -- need to use trig
+ // TODO: Implement this
+ }
+ */
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java
new file mode 100644
index 000000000..a35d19078
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2010 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.utils.Pair;
+
+import org.eclipse.swt.events.KeyEvent;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A gesture is a mouse or keyboard driven user operation, such as a
+ * swipe-select or a resize. It can be thought of as a session, since it is
+ * initiated, updated during user manipulation, and finally completed or
+ * canceled. A gesture is associated with a single undo transaction (although
+ * some gestures don't actually edit anything, such as a selection), and a
+ * gesture can have a number of graphics {@link Overlay}s which are added and
+ * cleaned up on behalf of the gesture by the system.
+ * <p/>
+ * Gestures are typically mouse oriented. If a mouse wishes to integrate
+ * with the native drag &amp; drop support, it should also implement
+ * the {@link DropGesture} interface, which is a sub interface of this
+ * {@link Gesture} interface. There are pros and cons to using native drag
+ * &amp; drop, so various gestures will differ in whether they use it.
+ * In particular, you should use drag &amp; drop if your gesture should:
+ * <ul>
+ * <li> Show a native drag &amp; drop cursor
+ * <li> Copy or move data, especially if this applies outside the canvas
+ * control window or even the application itself
+ * </ul>
+ * You might want to avoid using native drag &amp; drop if your gesture should:
+ * <ul>
+ * <li> Continue updating itself even when the mouse cursor leaves the
+ * canvas window (in a drag &amp; gesture, as soon as you leave the canvas
+ * the drag source is no longer informed of mouse updates, whereas a regular
+ * mouse listener is)
+ * <li> Respond to modifier keys (for example, if toggling the Shift key
+ * should constrain motion as is common during resizing, and so on)
+ * <li> Use no special cursor (for example, during a marquee selection gesture we
+ * don't want a native drag &amp; drop cursor)
+ * </ul>
+ * <p/>
+ * Examples of gestures:
+ * <ul>
+ * <li>Move (dragging to reorder or change hierarchy of views or change visual
+ * layout attributes)
+ * <li>Marquee (swiping out a rectangle to make a selection)
+ * <li>Resize (dragging some edge or corner of a widget to change its size, for
+ * example to some new fixed size, or to "attach" it to some other edge.)
+ * <li>Inline Editing (editing the text of some text-oriented widget like a
+ * label or a button)
+ * <li>Link (associate two or more widgets in some way, such as an
+ * "is required" widget linked to a text field)
+ * </ul>
+ */
+public abstract class Gesture {
+ /** Start mouse coordinate, in control coordinates. */
+ protected ControlPoint mStart;
+
+ /** Initial SWT mask when the gesture started. */
+ protected int mStartMask;
+
+ /**
+ * Returns a list of overlays, from bottom to top (where the later overlays
+ * are painted on top of earlier ones if they overlap).
+ *
+ * @return A list of overlays to paint for this gesture, if applicable.
+ * Should not be null, but can be empty.
+ */
+ public List<Overlay> createOverlays() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Handles initialization of this gesture. Called when the gesture is
+ * starting.
+ *
+ * @param pos The most recent mouse coordinate applicable to this
+ * gesture, relative to the canvas control.
+ * @param startMask The initial SWT mask for the gesture, if known, or
+ * otherwise 0.
+ */
+ public void begin(ControlPoint pos, int startMask) {
+ mStart = pos;
+ mStartMask = startMask;
+ }
+
+ /**
+ * Handles updating of the gesture state for a new mouse position.
+ *
+ * @param pos The most recent mouse coordinate applicable to this
+ * gesture, relative to the canvas control.
+ */
+ public void update(ControlPoint pos) {
+ }
+
+ /**
+ * Handles termination of the gesture. This method is called when the
+ * gesture has terminated (either through successful completion, or because
+ * it was canceled).
+ *
+ * @param pos The most recent mouse coordinate applicable to this
+ * gesture, relative to the canvas control.
+ * @param canceled True if the gesture was canceled, and false otherwise.
+ */
+ public void end(ControlPoint pos, boolean canceled) {
+ }
+
+ /**
+ * Handles a key press during the gesture. May be called repeatedly when the
+ * user is holding the key for several seconds.
+ *
+ * @param event The SWT event for the key press,
+ * @return true if this gesture consumed the key press, otherwise return false
+ */
+ public boolean keyPressed(KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Handles a key release during the gesture.
+ *
+ * @param event The SWT event for the key release,
+ * @return true if this gesture consumed the key press, otherwise return false
+ */
+ public boolean keyReleased(KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Returns whether tooltips should be display below and to the right of the mouse
+ * cursor.
+ *
+ * @return a pair of booleans, the first indicating whether the tooltip should be
+ * below and the second indicating whether the tooltip should be displayed to
+ * the right of the mouse cursor.
+ */
+ public Pair<Boolean, Boolean> getTooltipPosition() {
+ return Pair.of(true, true);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java
new file mode 100644
index 000000000..98bc25e37
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java
@@ -0,0 +1,930 @@
+/*
+ * Copyright (C) 2010 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.ide.common.api.DropFeedback;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.utils.Pair;
+
+import org.eclipse.jface.action.IStatusLineManager;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DragSource;
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.dnd.DropTarget;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.DropTargetListener;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.events.TypedEvent;
+import org.eclipse.swt.graphics.Cursor;
+import org.eclipse.swt.graphics.Device;
+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.Display;
+import org.eclipse.ui.IEditorSite;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The {@link GestureManager} is is the central manager of gestures; it is responsible
+ * for recognizing when particular gestures should begin and terminate. It
+ * listens to the drag, mouse and keyboard systems to find out when to start
+ * gestures and in order to update the gestures along the way.
+ */
+public class GestureManager {
+ /** The canvas which owns this GestureManager. */
+ private final LayoutCanvas mCanvas;
+
+ /** The currently executing gesture, or null. */
+ private Gesture mCurrentGesture;
+
+ /** A listener for drop target events. */
+ private final DropTargetListener mDropListener = new CanvasDropListener();
+
+ /** A listener for drag source events. */
+ private final DragSourceListener mDragSourceListener = new CanvasDragSourceListener();
+
+ /** Tooltip shown during the gesture, or null */
+ private GestureToolTip mTooltip;
+
+ /**
+ * The list of overlays associated with {@link #mCurrentGesture}. Will be
+ * null before it has been initialized lazily by the paint routine (the
+ * initialized value can never be null, but it can be an empty collection).
+ */
+ private List<Overlay> mOverlays;
+
+ /**
+ * Most recently seen mouse position (x coordinate). We keep a copy of this
+ * value since we sometimes need to know it when we aren't told about the
+ * mouse position (such as when a keystroke is received, such as an arrow
+ * key in order to tweak the current drop position)
+ */
+ protected int mLastMouseX;
+
+ /**
+ * Most recently seen mouse position (y coordinate). We keep a copy of this
+ * value since we sometimes need to know it when we aren't told about the
+ * mouse position (such as when a keystroke is received, such as an arrow
+ * key in order to tweak the current drop position)
+ */
+ protected int mLastMouseY;
+
+ /**
+ * Most recently seen mouse mask. We keep a copy of this since in some
+ * scenarios (such as on a drag gesture) we don't get access to it.
+ */
+ protected int mLastStateMask;
+
+ /**
+ * Listener for mouse motion, click and keyboard events.
+ */
+ private Listener mListener;
+
+ /**
+ * When we the drag leaves, we don't know if that's the last we'll see of
+ * this drag or if it's just temporarily outside the canvas and it will
+ * return. We want to restore it if it comes back. This is also necessary
+ * because even on a drop we'll receive a
+ * {@link DropTargetListener#dragLeave} right before the drop, and we need
+ * to restore it in the drop. Therefore, when we lose a {@link DropGesture}
+ * to a {@link DropTargetListener#dragLeave}, we store a reference to the
+ * current gesture as a {@link #mZombieGesture}, since the gesture is dead
+ * but might be brought back to life if we see a subsequent
+ * {@link DropTargetListener#dragEnter} before another gesture begins.
+ */
+ private DropGesture mZombieGesture;
+
+ /**
+ * Flag tracking whether we've set a message or error message on the global status
+ * line (since we only want to clear that message if we have set it ourselves).
+ * This is the actual message rather than a boolean such that (if we can get our
+ * hands on the global message) we can check to see if the current message is the
+ * one we set and only in that case clear it when it is no longer applicable.
+ */
+ private String mDisplayingMessage;
+
+ /**
+ * Constructs a new {@link GestureManager} for the given
+ * {@link LayoutCanvas}.
+ *
+ * @param canvas The canvas which controls this {@link GestureManager}
+ */
+ public GestureManager(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ }
+
+ /**
+ * Returns the canvas associated with this GestureManager.
+ *
+ * @return The {@link LayoutCanvas} associated with this GestureManager.
+ * Never null.
+ */
+ public LayoutCanvas getCanvas() {
+ return mCanvas;
+ }
+
+ /**
+ * Returns the current gesture, if one is in progress, and otherwise returns
+ * null.
+ *
+ * @return The current gesture or null.
+ */
+ public Gesture getCurrentGesture() {
+ return mCurrentGesture;
+ }
+
+ /**
+ * Paints the overlays associated with the current gesture, if any.
+ *
+ * @param gc The graphics object to paint into.
+ */
+ public void paint(GC gc) {
+ if (mCurrentGesture == null) {
+ return;
+ }
+
+ if (mOverlays == null) {
+ mOverlays = mCurrentGesture.createOverlays();
+ Device device = gc.getDevice();
+ for (Overlay overlay : mOverlays) {
+ overlay.create(device);
+ }
+ }
+ for (Overlay overlay : mOverlays) {
+ overlay.paint(gc);
+ }
+ }
+
+ /**
+ * Registers all the listeners needed by the {@link GestureManager}.
+ *
+ * @param dragSource The drag source in the {@link LayoutCanvas} to listen
+ * to.
+ * @param dropTarget The drop target in the {@link LayoutCanvas} to listen
+ * to.
+ */
+ public void registerListeners(DragSource dragSource, DropTarget dropTarget) {
+ assert mListener == null;
+ mListener = new Listener();
+ mCanvas.addMouseMoveListener(mListener);
+ mCanvas.addMouseListener(mListener);
+ mCanvas.addKeyListener(mListener);
+
+ if (dragSource != null) {
+ dragSource.addDragListener(mDragSourceListener);
+ }
+ if (dropTarget != null) {
+ dropTarget.addDropListener(mDropListener);
+ }
+ }
+
+ /**
+ * Unregisters all the listeners previously registered by
+ * {@link #registerListeners}.
+ *
+ * @param dragSource The drag source in the {@link LayoutCanvas} to stop
+ * listening to.
+ * @param dropTarget The drop target in the {@link LayoutCanvas} to stop
+ * listening to.
+ */
+ public void unregisterListeners(DragSource dragSource, DropTarget dropTarget) {
+ if (mCanvas.isDisposed()) {
+ // If the LayoutCanvas is already disposed, we shouldn't try to unregister
+ // the listeners; they are already not active and an attempt to remove the
+ // listener will throw a widget-is-disposed exception.
+ mListener = null;
+ return;
+ }
+
+ if (mListener != null) {
+ mCanvas.removeMouseMoveListener(mListener);
+ mCanvas.removeMouseListener(mListener);
+ mCanvas.removeKeyListener(mListener);
+ mListener = null;
+ }
+
+ if (dragSource != null) {
+ dragSource.removeDragListener(mDragSourceListener);
+ }
+ if (dropTarget != null) {
+ dropTarget.removeDropListener(mDropListener);
+ }
+ }
+
+ /**
+ * Starts the given gesture.
+ *
+ * @param mousePos The most recent mouse coordinate applicable to the new
+ * gesture, in control coordinates.
+ * @param gesture The gesture to initiate
+ */
+ private void startGesture(ControlPoint mousePos, Gesture gesture, int mask) {
+ if (mCurrentGesture != null) {
+ finishGesture(mousePos, true);
+ assert mCurrentGesture == null;
+ }
+
+ if (gesture != null) {
+ mCurrentGesture = gesture;
+ mCurrentGesture.begin(mousePos, mask);
+ }
+ }
+
+ /**
+ * Updates the current gesture, if any, for the given event.
+ *
+ * @param mousePos The most recent mouse coordinate applicable to the new
+ * gesture, in control coordinates.
+ * @param event The event corresponding to this update. May be null. Don't
+ * make any assumptions about the type of this event - for
+ * example, it may not always be a MouseEvent, it could be a
+ * DragSourceEvent, etc.
+ */
+ private void updateMouse(ControlPoint mousePos, TypedEvent event) {
+ if (mCurrentGesture != null) {
+ mCurrentGesture.update(mousePos);
+ }
+ }
+
+ /**
+ * Finish the given gesture, either from successful completion or from
+ * cancellation.
+ *
+ * @param mousePos The most recent mouse coordinate applicable to the new
+ * gesture, in control coordinates.
+ * @param canceled True if and only if the gesture was canceled.
+ */
+ private void finishGesture(ControlPoint mousePos, boolean canceled) {
+ if (mCurrentGesture != null) {
+ mCurrentGesture.end(mousePos, canceled);
+ if (mOverlays != null) {
+ for (Overlay overlay : mOverlays) {
+ overlay.dispose();
+ }
+ mOverlays = null;
+ }
+ mCurrentGesture = null;
+ mZombieGesture = null;
+ mLastStateMask = 0;
+ updateMessage(null);
+ updateCursor(mousePos);
+ mCanvas.redraw();
+ }
+ }
+
+ /**
+ * Update the cursor to show the type of operation we expect on a mouse press:
+ * <ul>
+ * <li>Over a selection handle, show a directional cursor depending on the position of
+ * the selection handle
+ * <li>Over a widget, show a move (hand) cursor
+ * <li>Otherwise, show the default arrow cursor
+ * </ul>
+ */
+ void updateCursor(ControlPoint controlPoint) {
+ // We don't hover on the root since it's not a widget per see and it is always there.
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+
+ if (!selectionManager.isEmpty()) {
+ Display display = mCanvas.getDisplay();
+ Pair<SelectionItem, SelectionHandle> handlePair =
+ selectionManager.findHandle(controlPoint);
+ if (handlePair != null) {
+ SelectionHandle handle = handlePair.getSecond();
+ int cursorType = handle.getSwtCursorType();
+ Cursor cursor = display.getSystemCursor(cursorType);
+ if (cursor != mCanvas.getCursor()) {
+ mCanvas.setCursor(cursor);
+ }
+ return;
+ }
+
+ // See if it's over a selected view
+ LayoutPoint layoutPoint = controlPoint.toLayout();
+ for (SelectionItem item : selectionManager.getSelections()) {
+ if (item.getRect().contains(layoutPoint.x, layoutPoint.y)
+ && !item.isRoot()) {
+ Cursor cursor = display.getSystemCursor(SWT.CURSOR_HAND);
+ if (cursor != mCanvas.getCursor()) {
+ mCanvas.setCursor(cursor);
+ }
+ return;
+ }
+ }
+ }
+
+ if (mCanvas.getCursor() != null) {
+ mCanvas.setCursor(null);
+ }
+ }
+
+ /**
+ * Update the Eclipse status message with any feedback messages from the given
+ * {@link DropFeedback} object, or clean up if there is no more feedback to process
+ * @param feedback the feedback whose message we want to display, or null to clear the
+ * message if previously set
+ */
+ void updateMessage(DropFeedback feedback) {
+ IEditorSite editorSite = mCanvas.getEditorDelegate().getEditor().getEditorSite();
+ IStatusLineManager status = editorSite.getActionBars().getStatusLineManager();
+ if (feedback == null) {
+ if (mDisplayingMessage != null) {
+ status.setMessage(null);
+ status.setErrorMessage(null);
+ mDisplayingMessage = null;
+ }
+ } else if (feedback.errorMessage != null) {
+ if (!feedback.errorMessage.equals(mDisplayingMessage)) {
+ mDisplayingMessage = feedback.errorMessage;
+ status.setErrorMessage(mDisplayingMessage);
+ }
+ } else if (feedback.message != null) {
+ if (!feedback.message.equals(mDisplayingMessage)) {
+ mDisplayingMessage = feedback.message;
+ status.setMessage(mDisplayingMessage);
+ }
+ } else if (mDisplayingMessage != null) {
+ // TODO: Can we check the existing message and only clear it if it's the
+ // same as the one we set?
+ mDisplayingMessage = null;
+ status.setMessage(null);
+ status.setErrorMessage(null);
+ }
+
+ // Tooltip
+ if (feedback != null && feedback.tooltip != null) {
+ Pair<Boolean,Boolean> position = mCurrentGesture.getTooltipPosition();
+ boolean below = position.getFirst();
+ if (feedback.tooltipY != null) {
+ below = feedback.tooltipY == SegmentType.BOTTOM;
+ }
+ boolean toRightOf = position.getSecond();
+ if (feedback.tooltipX != null) {
+ toRightOf = feedback.tooltipX == SegmentType.RIGHT;
+ }
+ if (mTooltip == null) {
+ mTooltip = new GestureToolTip(mCanvas, below, toRightOf);
+ }
+ mTooltip.update(feedback.tooltip, below, toRightOf);
+ } else if (mTooltip != null) {
+ mTooltip.dispose();
+ mTooltip = null;
+ }
+ }
+
+ /**
+ * Returns the current mouse position as a {@link ControlPoint}
+ *
+ * @return the current mouse position as a {@link ControlPoint}
+ */
+ public ControlPoint getCurrentControlPoint() {
+ return ControlPoint.create(mCanvas, mLastMouseX, mLastMouseY);
+ }
+
+ /**
+ * Returns the current SWT modifier key mask as an {@link IViewRule} modifier mask
+ *
+ * @return the current SWT modifier key mask as an {@link IViewRule} modifier mask
+ */
+ public int getRuleModifierMask() {
+ int swtMask = mLastStateMask;
+ int modifierMask = 0;
+ if ((swtMask & SWT.MOD1) != 0) {
+ modifierMask |= DropFeedback.MODIFIER1;
+ }
+ if ((swtMask & SWT.MOD2) != 0) {
+ modifierMask |= DropFeedback.MODIFIER2;
+ }
+ if ((swtMask & SWT.MOD3) != 0) {
+ modifierMask |= DropFeedback.MODIFIER3;
+ }
+ return modifierMask;
+ }
+
+ /**
+ * Helper class which implements the {@link MouseMoveListener},
+ * {@link MouseListener} and {@link KeyListener} interfaces.
+ */
+ private class Listener implements MouseMoveListener, MouseListener, MouseTrackListener,
+ KeyListener {
+
+ // --- MouseMoveListener ---
+
+ @Override
+ public void mouseMove(MouseEvent e) {
+ mLastMouseX = e.x;
+ mLastMouseY = e.y;
+ mLastStateMask = e.stateMask;
+
+ ControlPoint controlPoint = ControlPoint.create(mCanvas, e);
+ if ((e.stateMask & SWT.BUTTON_MASK) != 0) {
+ if (mCurrentGesture != null) {
+ updateMouse(controlPoint, e);
+ mCanvas.redraw();
+ }
+ } else {
+ updateCursor(controlPoint);
+ mCanvas.hover(e);
+ mCanvas.getPreviewManager().moved(controlPoint);
+ }
+ }
+
+ // --- MouseListener ---
+
+ @Override
+ public void mouseUp(MouseEvent e) {
+ ControlPoint mousePos = ControlPoint.create(mCanvas, e);
+
+ if (mCurrentGesture == null) {
+ // If clicking on a configuration preview, just process it there
+ if (mCanvas.getPreviewManager().click(mousePos)) {
+ return;
+ }
+
+ // Just a click, select
+ Pair<SelectionItem, SelectionHandle> handlePair =
+ mCanvas.getSelectionManager().findHandle(mousePos);
+ if (handlePair == null) {
+ mCanvas.getSelectionManager().select(e);
+ }
+ }
+ if (mCurrentGesture == null) {
+ updateCursor(mousePos);
+ } else if (mCurrentGesture instanceof DropGesture) {
+ // Mouse Up shouldn't be delivered in the middle of a drag & drop -
+ // but this can happen on some versions of Linux
+ // (see http://code.google.com/p/android/issues/detail?id=19057 )
+ // and if we process the mouseUp it will abort the remainder of
+ // the drag & drop operation, so ignore this event!
+ } else {
+ finishGesture(mousePos, false);
+ }
+ mCanvas.redraw();
+ }
+
+ @Override
+ public void mouseDown(MouseEvent e) {
+ mLastMouseX = e.x;
+ mLastMouseY = e.y;
+ mLastStateMask = e.stateMask;
+
+ // Not yet used. Should be, for Mac and Linux.
+ }
+
+ @Override
+ public void mouseDoubleClick(MouseEvent e) {
+ // SWT delivers a double click event even if you click two different buttons
+ // in rapid succession. In any case, we only want to let you double click the
+ // first button to warp to XML:
+ if (e.button == 1) {
+ // Warp to the text editor and show the corresponding XML for the
+ // double-clicked widget
+ LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout();
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+ if (vi != null) {
+ mCanvas.show(vi);
+ }
+ }
+ }
+
+ // --- MouseTrackListener ---
+
+ @Override
+ public void mouseEnter(MouseEvent e) {
+ ControlPoint mousePos = ControlPoint.create(mCanvas, e);
+ mCanvas.getPreviewManager().enter(mousePos);
+ }
+
+ @Override
+ public void mouseExit(MouseEvent e) {
+ ControlPoint mousePos = ControlPoint.create(mCanvas, e);
+ mCanvas.getPreviewManager().exit(mousePos);
+ }
+
+ @Override
+ public void mouseHover(MouseEvent e) {
+ }
+
+ // --- KeyListener ---
+
+ @Override
+ public void keyPressed(KeyEvent e) {
+ mLastStateMask = e.stateMask;
+ // Workaround for the fact that in keyPressed the current state
+ // mask is not yet updated
+ if (e.keyCode == SWT.SHIFT) {
+ mLastStateMask |= SWT.MOD2;
+ }
+ if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
+ if (e.keyCode == SWT.COMMAND) {
+ mLastStateMask |= SWT.MOD1;
+ }
+ } else {
+ if (e.keyCode == SWT.CTRL) {
+ mLastStateMask |= SWT.MOD1;
+ }
+ }
+
+ // Give gestures a first chance to see and consume the key press
+ if (mCurrentGesture != null) {
+ // unless it's "Escape", which cancels the gesture
+ if (e.keyCode == SWT.ESC) {
+ ControlPoint controlPoint = ControlPoint.create(mCanvas,
+ mLastMouseX, mLastMouseY);
+ finishGesture(controlPoint, true);
+ return;
+ }
+
+ if (mCurrentGesture.keyPressed(e)) {
+ return;
+ }
+ }
+
+ // Fall back to canvas actions for the key press
+ mCanvas.handleKeyPressed(e);
+ }
+
+ @Override
+ public void keyReleased(KeyEvent e) {
+ mLastStateMask = e.stateMask;
+ // Workaround for the fact that in keyPressed the current state
+ // mask is not yet updated
+ if (e.keyCode == SWT.SHIFT) {
+ mLastStateMask &= ~SWT.MOD2;
+ }
+ if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
+ if (e.keyCode == SWT.COMMAND) {
+ mLastStateMask &= ~SWT.MOD1;
+ }
+ } else {
+ if (e.keyCode == SWT.CTRL) {
+ mLastStateMask &= ~SWT.MOD1;
+ }
+ }
+
+ if (mCurrentGesture != null) {
+ mCurrentGesture.keyReleased(e);
+ }
+ }
+ }
+
+ /** Listener for Drag &amp; Drop events. */
+ private class CanvasDropListener implements DropTargetListener {
+ public CanvasDropListener() {
+ }
+
+ /**
+ * The cursor has entered the drop target boundaries. {@inheritDoc}
+ */
+ @Override
+ public void dragEnter(DropTargetEvent event) {
+ mCanvas.showInvisibleViews(true);
+ mCanvas.getEditorDelegate().getGraphicalEditor().dismissHoverPalette();
+
+ if (mCurrentGesture == null) {
+ Gesture newGesture = mZombieGesture;
+ if (newGesture == null) {
+ newGesture = new MoveGesture(mCanvas);
+ } else {
+ mZombieGesture = null;
+ }
+ startGesture(ControlPoint.create(mCanvas, event),
+ newGesture, 0);
+ }
+
+ if (mCurrentGesture instanceof DropGesture) {
+ ((DropGesture) mCurrentGesture).dragEnter(event);
+ }
+ }
+
+ /**
+ * The cursor is moving over the drop target. {@inheritDoc}
+ */
+ @Override
+ public void dragOver(DropTargetEvent event) {
+ if (mCurrentGesture instanceof DropGesture) {
+ ((DropGesture) mCurrentGesture).dragOver(event);
+ }
+ }
+
+ /**
+ * The cursor has left the drop target boundaries OR data is about to be
+ * dropped. {@inheritDoc}
+ */
+ @Override
+ public void dragLeave(DropTargetEvent event) {
+ if (mCurrentGesture instanceof DropGesture) {
+ DropGesture dropGesture = (DropGesture) mCurrentGesture;
+ dropGesture.dragLeave(event);
+ finishGesture(ControlPoint.create(mCanvas, event), true);
+ mZombieGesture = dropGesture;
+ }
+
+ mCanvas.showInvisibleViews(false);
+ }
+
+ /**
+ * The drop is about to be performed. The drop target is given a last
+ * chance to change the nature of the drop. {@inheritDoc}
+ */
+ @Override
+ public void dropAccept(DropTargetEvent event) {
+ Gesture gesture = mCurrentGesture != null ? mCurrentGesture : mZombieGesture;
+ if (gesture instanceof DropGesture) {
+ ((DropGesture) gesture).dropAccept(event);
+ }
+ }
+
+ /**
+ * The data is being dropped. {@inheritDoc}
+ */
+ @Override
+ public void drop(final DropTargetEvent event) {
+ // See if we had a gesture just prior to the drop (we receive a dragLeave
+ // right before the drop which we don't know whether means the cursor has
+ // left the canvas for good or just before a drop)
+ Gesture gesture = mCurrentGesture != null ? mCurrentGesture : mZombieGesture;
+ mZombieGesture = null;
+
+ if (gesture instanceof DropGesture) {
+ ((DropGesture) gesture).drop(event);
+
+ finishGesture(ControlPoint.create(mCanvas, event), true);
+ }
+ }
+
+ /**
+ * The operation being performed has changed (e.g. modifier key).
+ * {@inheritDoc}
+ */
+ @Override
+ public void dragOperationChanged(DropTargetEvent event) {
+ if (mCurrentGesture instanceof DropGesture) {
+ ((DropGesture) mCurrentGesture).dragOperationChanged(event);
+ }
+ }
+ }
+
+ /**
+ * Our canvas {@link DragSourceListener}. Handles drag being started and
+ * finished and generating the drag data.
+ */
+ private class CanvasDragSourceListener implements DragSourceListener {
+
+ /**
+ * The current selection being dragged. This may be a subset of the
+ * canvas selection due to the "sanitize" pass. Can be empty but never
+ * null.
+ */
+ private final ArrayList<SelectionItem> mDragSelection = new ArrayList<SelectionItem>();
+
+ private SimpleElement[] mDragElements;
+
+ /**
+ * The user has begun the actions required to drag the widget.
+ * <p/>
+ * Initiate a drag only if there is one or more item selected. If
+ * there's none, try to auto-select the one under the cursor.
+ * {@inheritDoc}
+ */
+ @Override
+ public void dragStart(DragSourceEvent e) {
+ LayoutPoint p = LayoutPoint.create(mCanvas, e);
+ ControlPoint controlPoint = ControlPoint.create(mCanvas, e);
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+
+ // See if the mouse is over a selection handle; if so, start a resizing
+ // gesture.
+ Pair<SelectionItem, SelectionHandle> handle =
+ selectionManager.findHandle(controlPoint);
+ if (handle != null) {
+ startGesture(controlPoint, new ResizeGesture(mCanvas, handle.getFirst(),
+ handle.getSecond()), mLastStateMask);
+ e.detail = DND.DROP_NONE;
+ e.doit = false;
+ mCanvas.redraw();
+ return;
+ }
+
+ // We need a selection (simple or multiple) to do any transfer.
+ // If there's a selection *and* the cursor is over this selection,
+ // use all the currently selected elements.
+ // If there is no selection or the cursor is not over a selected
+ // element, *change* the selection to match the element under the
+ // cursor and use that. If nothing can be selected, abort the drag
+ // operation.
+ List<SelectionItem> selections = selectionManager.getSelections();
+ mDragSelection.clear();
+ SelectionItem primary = null;
+
+ if (!selections.isEmpty()) {
+ // Is the cursor on top of a selected element?
+ boolean insideSelection = false;
+
+ for (SelectionItem cs : selections) {
+ if (!cs.isRoot() && cs.getRect().contains(p.x, p.y)) {
+ primary = cs;
+ insideSelection = true;
+ break;
+ }
+ }
+
+ if (!insideSelection) {
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+ if (vi != null && !vi.isRoot() && !vi.isHidden()) {
+ primary = selectionManager.selectSingle(vi);
+ insideSelection = true;
+ }
+ }
+
+ if (insideSelection) {
+ // We should now have a proper selection that matches the
+ // cursor. Let's use this one. We make a copy of it since
+ // the "sanitize" pass below might remove some of the
+ // selected objects.
+ if (selections.size() == 1) {
+ // You are dragging just one element - this might or
+ // might not be the root, but if it's the root that is
+ // fine since we will let you drag the root if it is the
+ // only thing you are dragging.
+ mDragSelection.addAll(selections);
+ } else {
+ // Only drag non-root items.
+ for (SelectionItem cs : selections) {
+ if (!cs.isRoot() && !cs.isHidden()) {
+ mDragSelection.add(cs);
+ } else if (cs == primary) {
+ primary = null;
+ }
+ }
+ }
+ }
+ }
+
+ // If you are dragging a non-selected item, select it
+ if (mDragSelection.isEmpty()) {
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+ if (vi != null && !vi.isRoot() && !vi.isHidden()) {
+ primary = selectionManager.selectSingle(vi);
+ mDragSelection.addAll(selections);
+ }
+ }
+
+ SelectionManager.sanitize(mDragSelection);
+
+ e.doit = !mDragSelection.isEmpty();
+ int imageCount = mDragSelection.size();
+ if (e.doit) {
+ mDragElements = SelectionItem.getAsElements(mDragSelection, primary);
+ GlobalCanvasDragInfo.getInstance().startDrag(mDragElements,
+ mDragSelection.toArray(new SelectionItem[imageCount]),
+ mCanvas, new Runnable() {
+ @Override
+ public void run() {
+ mCanvas.getClipboardSupport().deleteSelection("Remove",
+ mDragSelection);
+ }
+ });
+ }
+
+ // If you drag on the -background-, we make that into a marquee
+ // selection
+ if (!e.doit || (imageCount == 1
+ && (mDragSelection.get(0).isRoot() || mDragSelection.get(0).isHidden()))) {
+ boolean toggle = (mLastStateMask & (SWT.CTRL | SWT.SHIFT | SWT.COMMAND)) != 0;
+ startGesture(controlPoint,
+ new MarqueeGesture(mCanvas, toggle), mLastStateMask);
+ e.detail = DND.DROP_NONE;
+ e.doit = false;
+ } else {
+ // Otherwise, the drag means you are moving something
+ mCanvas.showInvisibleViews(true);
+ startGesture(controlPoint, new MoveGesture(mCanvas), 0);
+
+ // Render drag-images: Copy portions of the full screen render.
+ Image image = mCanvas.getImageOverlay().getImage();
+ if (image != null) {
+ /**
+ * Transparency of the dragged image ([0-255]). We're using 30%
+ * translucency to make the image faint and not obscure the drag
+ * feedback below it.
+ */
+ final byte DRAG_TRANSPARENCY = (byte) (0.3 * 255);
+
+ List<Rectangle> rectangles = new ArrayList<Rectangle>(imageCount);
+ if (imageCount > 0) {
+ ImageData data = image.getImageData();
+ Rectangle imageRectangle = new Rectangle(0, 0, data.width, data.height);
+ for (SelectionItem item : mDragSelection) {
+ Rectangle bounds = item.getRect();
+ // Some bounds can be outside the rendered rectangle (for
+ // example, in an absolute layout, you can have negative
+ // coordinates), so create the intersection of these bounds.
+ Rectangle clippedBounds = imageRectangle.intersection(bounds);
+ rectangles.add(clippedBounds);
+ }
+ Rectangle boundingBox = ImageUtils.getBoundingRectangle(rectangles);
+ double scale = mCanvas.getHorizontalTransform().getScale();
+ e.image = SwtUtils.drawRectangles(image, rectangles, boundingBox, scale,
+ DRAG_TRANSPARENCY);
+
+ // Set the image offset such that we preserve the relative
+ // distance between the mouse pointer and the top left corner of
+ // the dragged view
+ int deltaX = (int) (scale * (boundingBox.x - p.x));
+ int deltaY = (int) (scale * (boundingBox.y - p.y));
+ e.offsetX = -deltaX;
+ e.offsetY = -deltaY;
+
+ // View rules may need to know it as well
+ GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance();
+ Rect dragBounds = null;
+ int width = (int) (scale * boundingBox.width);
+ int height = (int) (scale * boundingBox.height);
+ dragBounds = new Rect(deltaX, deltaY, width, height);
+ dragInfo.setDragBounds(dragBounds);
+
+ // Record the baseline such that we can perform baseline alignment
+ // on the node as it's dragged around
+ NodeProxy firstNode =
+ mCanvas.getNodeFactory().create(mDragSelection.get(0).getViewInfo());
+ dragInfo.setDragBaseline(firstNode.getBaseline());
+ }
+ }
+ }
+
+ // No hover during drag (since no mouse over events are delivered
+ // during a drag to keep the hovers up to date anyway)
+ mCanvas.clearHover();
+
+ mCanvas.redraw();
+ }
+
+ /**
+ * Callback invoked when data is needed for the event, typically right
+ * before drop. The drop side decides what type of transfer to use and
+ * this side must now provide the adequate data. {@inheritDoc}
+ */
+ @Override
+ public void dragSetData(DragSourceEvent e) {
+ if (TextTransfer.getInstance().isSupportedType(e.dataType)) {
+ e.data = SelectionItem.getAsText(mCanvas, mDragSelection);
+ return;
+ }
+
+ if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) {
+ e.data = mDragElements;
+ return;
+ }
+
+ // otherwise we failed
+ e.detail = DND.DROP_NONE;
+ e.doit = false;
+ }
+
+ /**
+ * Callback invoked when the drop has been finished either way. On a
+ * successful move, remove the originating elements.
+ */
+ @Override
+ public void dragFinished(DragSourceEvent e) {
+ // Clear the selection
+ mDragSelection.clear();
+ mDragElements = null;
+ GlobalCanvasDragInfo.getInstance().stopDrag();
+
+ finishGesture(ControlPoint.create(mCanvas, e), e.detail == DND.DROP_NONE);
+ mCanvas.showInvisibleViews(false);
+ mCanvas.redraw();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureToolTip.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureToolTip.java
new file mode 100644
index 000000000..a49e79cbf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureToolTip.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2011 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 org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * A dedicated tooltip used during gestures, for example to show the resize dimensions.
+ * <p>
+ * This is necessary because {@link org.eclipse.jface.window.ToolTip} causes flicker when
+ * used to dynamically update the position and text of the tip, and it does not seem to
+ * have setter methods to update the text or position without recreating the tip.
+ */
+public class GestureToolTip {
+ /** Minimum number of milliseconds to wait between alignment changes */
+ private static final int TIMEOUT_MS = 750;
+
+ /**
+ * The alpha to use for the tooltip window (which sadly will apply to the tooltip text
+ * as well.)
+ */
+ private static final int SHELL_TRANSPARENCY = 220;
+
+ /** The size of the font displayed in the tooltip */
+ private static final int FONT_SIZE = 9;
+
+ /** Horizontal delta from the mouse cursor to shift the tooltip by */
+ private static final int OFFSET_X = 20;
+
+ /** Vertical delta from the mouse cursor to shift the tooltip by */
+ private static final int OFFSET_Y = 20;
+
+ /** The label which displays the tooltip */
+ private CLabel mLabel;
+
+ /** The shell holding the tooltip */
+ private Shell mShell;
+
+ /** The font shown in the label; held here such that it can be disposed of after use */
+ private Font mFont;
+
+ /** Is the tooltip positioned below the given anchor? */
+ private boolean mBelow;
+
+ /** Is the tooltip positioned to the right of the given anchor? */
+ private boolean mToRightOf;
+
+ /** Is an alignment change pending? */
+ private boolean mTimerPending;
+
+ /** The new value for {@link #mBelow} when the timer expires */
+ private boolean mPendingBelow;
+
+ /** The new value for {@link #mToRightOf} when the timer expires */
+ private boolean mPendingRight;
+
+ /** The time stamp (from {@link System#currentTimeMillis()} of the last alignment change */
+ private long mLastAlignmentTime;
+
+ /**
+ * Creates a new tooltip over the given parent with the given relative position.
+ *
+ * @param parent the parent control
+ * @param below if true, display the tooltip below the mouse cursor otherwise above
+ * @param toRightOf if true, display the tooltip to the right of the mouse cursor,
+ * otherwise to the left
+ */
+ public GestureToolTip(Composite parent, boolean below, boolean toRightOf) {
+ mBelow = below;
+ mToRightOf = toRightOf;
+ mLastAlignmentTime = System.currentTimeMillis();
+
+ mShell = new Shell(parent.getShell(), SWT.ON_TOP | SWT.TOOL | SWT.NO_FOCUS);
+ mShell.setLayout(new FillLayout());
+ mShell.setAlpha(SHELL_TRANSPARENCY);
+
+ Display display = parent.getDisplay();
+ mLabel = new CLabel(mShell, SWT.SHADOW_NONE);
+ mLabel.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND));
+ mLabel.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND));
+
+ Font systemFont = display.getSystemFont();
+ FontData[] fd = systemFont.getFontData();
+ for (int i = 0; i < fd.length; i++) {
+ fd[i].setHeight(FONT_SIZE);
+ }
+ mFont = new Font(display, fd);
+ mLabel.setFont(mFont);
+
+ mShell.setVisible(false);
+ }
+
+ /**
+ * Show the tooltip at the given position and with the given text. Note that the
+ * position may not be applied immediately; to prevent flicker alignment changes
+ * are queued up with a timer (unless it's been a while since the last change, in
+ * which case the update is applied immediately.)
+ *
+ * @param text the new text to be displayed
+ * @param below if true, display the tooltip below the mouse cursor otherwise above
+ * @param toRightOf if true, display the tooltip to the right of the mouse cursor,
+ * otherwise to the left
+ */
+ public void update(final String text, boolean below, boolean toRightOf) {
+ // If the alignment has not changed recently, just apply the change immediately
+ // instead of within a delay
+ if (!mTimerPending && (below != mBelow || toRightOf != mToRightOf)
+ && (System.currentTimeMillis() - mLastAlignmentTime >= TIMEOUT_MS)) {
+ mBelow = below;
+ mToRightOf = toRightOf;
+ mLastAlignmentTime = System.currentTimeMillis();
+ }
+
+ Point location = mShell.getDisplay().getCursorLocation();
+
+ mLabel.setText(text);
+
+ // Pack the label to its minimum size -- unless we are positioning the tooltip
+ // on the left. Because of the way SWT works (at least on the OSX) this sometimes
+ // creates flicker, because when we switch to a longer string (such as when
+ // switching from "52dp" to "wrap_content" during a resize) the window size will
+ // change first, and then the location will update later - so there will be a
+ // brief flash of the longer label before it is moved to the right position on the
+ // left. To work around this, we simply pass false to pack such that it will reuse
+ // its cached size, which in practice means that for labels on the right, the
+ // label will grow but not shrink.
+ // This workaround is disabled because it doesn't work well in Eclipse 3.5; the
+ // labels don't grow when they should. Re-enable when we drop 3.5 support.
+ //boolean changed = mToRightOf;
+ boolean changed = true;
+
+ mShell.pack(changed);
+ Point size = mShell.getSize();
+
+ // Position the tooltip to the left or right, and above or below, according
+ // to the saved state of these flags, not the current parameters. We don't want
+ // to flicker, instead we react on a timer to changes in alignment below.
+ if (mBelow) {
+ location.y += OFFSET_Y;
+ } else {
+ location.y -= OFFSET_Y;
+ location.y -= size.y;
+ }
+
+ if (mToRightOf) {
+ location.x += OFFSET_X;
+ } else {
+ location.x -= OFFSET_X;
+ location.x -= size.x;
+ }
+
+ mShell.setLocation(location);
+
+ if (!mShell.isVisible()) {
+ mShell.setVisible(true);
+ }
+
+ // Has the orientation changed?
+ mPendingBelow = below;
+ mPendingRight = toRightOf;
+ if (below != mBelow || toRightOf != mToRightOf) {
+ // Yes, so schedule a timer (unless one is already scheduled)
+ if (!mTimerPending) {
+ mTimerPending = true;
+ final Runnable timer = new Runnable() {
+ @Override
+ public void run() {
+ mTimerPending = false;
+ // Check whether the alignment is still different than the target
+ // (since we may change back and forth repeatedly during the timeout)
+ if (mBelow != mPendingBelow || mToRightOf != mPendingRight) {
+ mBelow = mPendingBelow;
+ mToRightOf = mPendingRight;
+ mLastAlignmentTime = System.currentTimeMillis();
+ if (mShell != null && mShell.isVisible()) {
+ update(text, mBelow, mToRightOf);
+ }
+ }
+ }
+ };
+ mShell.getDisplay().timerExec(TIMEOUT_MS, timer);
+ }
+ }
+ }
+
+ /** Hide the tooltip and dispose of any associated resources */
+ public void dispose() {
+ mShell.dispose();
+ mFont.dispose();
+
+ mShell = null;
+ mFont = null;
+ mLabel = null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GlobalCanvasDragInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GlobalCanvasDragInfo.java
new file mode 100644
index 000000000..b918b00bf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GlobalCanvasDragInfo.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2010 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.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.Rect;
+
+
+/**
+ * This singleton is used to keep track of drag'n'drops initiated within this
+ * session of Eclipse. A drag can be initiated from a palette or from a canvas
+ * and its content is an Android View fully-qualified class name.
+ * <p/>
+ * Overall this is a workaround: the issue is that the drag'n'drop SWT API does not
+ * allow us to know the transfered data during the initial drag -- only when the
+ * data is dropped do we know what it is about (and to be more exact there is a workaround
+ * to do just that which works on Windows but not on Linux/Mac SWT).
+ * <p/>
+ * In the GLE we'd like to adjust drag feedback to the data being actually dropped.
+ * The singleton instance of this class will be used to track the data currently dragged
+ * off a canvas or its palette and then set back to null when the drag'n'drop is finished.
+ * <p/>
+ * Note that when a drag starts in one instance of Eclipse and the dragOver/drop is done
+ * in a <em>separate</em> instance of Eclipse, the dragged FQCN won't be registered here
+ * and will be null.
+ */
+final class GlobalCanvasDragInfo {
+
+ private static final GlobalCanvasDragInfo sInstance = new GlobalCanvasDragInfo();
+
+ private SimpleElement[] mCurrentElements = null;
+ private SelectionItem[] mCurrentSelection;
+ private Object mSourceCanvas = null;
+ private Runnable mRemoveSourceHandler;
+ private Rect mDragBounds;
+ private int mDragBaseline = -1;
+
+ /** Private constructor. Use {@link #getInstance()} to retrieve the singleton. */
+ private GlobalCanvasDragInfo() {
+ // pass
+ }
+
+ /** Returns the singleton instance. */
+ public static GlobalCanvasDragInfo getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * Registers the XML elements being dragged.
+ *
+ * @param elements The elements being dragged
+ * @param primary the "primary" element among the elements; when there is a
+ * single item dragged this will be the same, but in
+ * multi-selection it will be the element under the mouse as the
+ * selection was initiated
+ * @param selection The selection (which can be null, for example when the
+ * user drags from the palette)
+ * @param sourceCanvas An object representing the source we are dragging
+ * from (used for identity comparisons only)
+ * @param removeSourceHandler A runnable (or null) which can clean up the
+ * source. It should only be invoked if the drag operation is a
+ * move, not a copy.
+ */
+ public void startDrag(
+ @NonNull SimpleElement[] elements,
+ @Nullable SelectionItem[] selection,
+ @Nullable Object sourceCanvas,
+ @Nullable Runnable removeSourceHandler) {
+ mCurrentElements = elements;
+ mCurrentSelection = selection;
+ mSourceCanvas = sourceCanvas;
+ mRemoveSourceHandler = removeSourceHandler;
+ }
+
+ /** Unregisters elements being dragged. */
+ public void stopDrag() {
+ mCurrentElements = null;
+ mCurrentSelection = null;
+ mSourceCanvas = null;
+ mRemoveSourceHandler = null;
+ mDragBounds = null;
+ }
+
+ public boolean isDragging() {
+ return mCurrentElements != null;
+ }
+
+ /** Returns the elements being dragged. */
+ @NonNull
+ public SimpleElement[] getCurrentElements() {
+ return mCurrentElements;
+ }
+
+ /** Returns the selection originally dragged.
+ * Can be null if the drag did not start in a canvas.
+ */
+ public SelectionItem[] getCurrentSelection() {
+ return mCurrentSelection;
+ }
+
+ /**
+ * Returns the object that call {@link #startDrag(SimpleElement[], SelectionItem[], Object)}.
+ * Can be null.
+ * This is not meant to access the object indirectly, it is just meant to compare if the
+ * source and the destination of the drag'n'drop are the same, so object identity
+ * is all what matters.
+ */
+ public Object getSourceCanvas() {
+ return mSourceCanvas;
+ }
+
+ /**
+ * Removes source of the drag. This should only be called when the drag and
+ * drop operation is a move (not a copy).
+ */
+ public void removeSource() {
+ if (mRemoveSourceHandler != null) {
+ mRemoveSourceHandler.run();
+ mRemoveSourceHandler = null;
+ }
+ }
+
+ /**
+ * Get the bounds of the drag, relative to the starting mouse position. For example,
+ * if you have a rectangular view of size 100x80, and you start dragging at position
+ * (15,20) from the top left corner of this rectangle, then the drag bounds would be
+ * (-15,-20, 100x80).
+ * <p>
+ * NOTE: The coordinate units will be in SWT/control pixels, not Android view pixels.
+ * In other words, they are affected by the canvas zoom: If you zoom the view and the
+ * bounds of a view grow, the drag bounds will be larger.
+ *
+ * @return the drag bounds, or null if there are no bounds for the current drag
+ */
+ public Rect getDragBounds() {
+ return mDragBounds;
+ }
+
+ /**
+ * Set the bounds of the drag, relative to the starting mouse position. See
+ * {@link #getDragBounds()} for details on the semantics of the drag bounds.
+ *
+ * @param dragBounds the new drag bounds, or null if there are no drag bounds
+ */
+ public void setDragBounds(Rect dragBounds) {
+ mDragBounds = dragBounds;
+ }
+
+ /**
+ * Returns the baseline of the drag, or -1 if not applicable
+ *
+ * @return the current SWT modifier key mask as an {@link IViewRule} modifier mask
+ */
+ public int getDragBaseline() {
+ return mDragBaseline;
+ }
+
+ /**
+ * Sets the baseline of the drag
+ *
+ * @param baseline the new baseline
+ */
+ public void setDragBaseline(int baseline) {
+ mDragBaseline = baseline;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java
new file mode 100644
index 000000000..0f5762da6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java
@@ -0,0 +1,2937 @@
+/*
+ * 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 static com.android.SdkConstants.ANDROID_PKG;
+import static com.android.SdkConstants.ANDROID_STRING_PREFIX;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_CONTEXT;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.FD_GEN_SOURCES;
+import static com.android.SdkConstants.GRID_LAYOUT;
+import static com.android.SdkConstants.SCROLL_VIEW;
+import static com.android.SdkConstants.STRING_PREFIX;
+import static com.android.SdkConstants.VALUE_FALSE;
+import static com.android.SdkConstants.VALUE_FILL_PARENT;
+import static com.android.SdkConstants.VALUE_MATCH_PARENT;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+import static com.android.ide.common.rendering.RenderSecurityManager.ENABLED_PROPERTY;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE_STATE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_FOLDER;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_TARGET;
+import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor.viewNeedsPackage;
+import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.DOCK_EAST;
+import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.DOCK_WEST;
+import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.STATE_COLLAPSED;
+import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.STATE_OPEN;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.layout.BaseLayoutRule;
+import com.android.ide.common.rendering.LayoutLibrary;
+import com.android.ide.common.rendering.RenderSecurityException;
+import com.android.ide.common.rendering.RenderSecurityManager;
+import com.android.ide.common.rendering.StaticRenderSession;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.rendering.api.Result;
+import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.ResourceResolver;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.sdk.LoadStatus;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ChangeFlags;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutReloadMonitor.ILayoutReloadListener;
+import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationMatcher;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.PaletteControl.PalettePage;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertyFactory;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+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.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
+import com.android.resources.Density;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.android.sdklib.IAndroidTarget;
+import com.android.tools.lint.detector.api.LintUtils;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaModelMarker;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.internal.ui.preferences.BuildPathsPropertyPage;
+import org.eclipse.jdt.ui.actions.OpenNewClassWizardAction;
+import org.eclipse.jdt.ui.wizards.NewClassWizardPage;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.custom.StyleRange;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.text.edits.MalformedTreeException;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.INullSelectionListener;
+import org.eclipse.ui.ISelectionListener;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchPartSite;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.dialogs.PreferencesUtil;
+import org.eclipse.ui.ide.IDE;
+import org.eclipse.ui.part.EditorPart;
+import org.eclipse.ui.part.FileEditorInput;
+import org.eclipse.ui.part.IPageSite;
+import org.eclipse.ui.part.PageBookView;
+import org.eclipse.wb.core.controls.flyout.FlyoutControlComposite;
+import org.eclipse.wb.core.controls.flyout.IFlyoutListener;
+import org.eclipse.wb.core.controls.flyout.PluginFlyoutPreferences;
+import org.eclipse.wb.internal.core.editor.structure.PageSiteComposite;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Graphical layout editor part, version 2.
+ * <p/>
+ * The main component of the editor part is the {@link LayoutCanvasViewer}, which
+ * actually delegates its work to the {@link LayoutCanvas} control.
+ * <p/>
+ * The {@link LayoutCanvasViewer} is set as the site's {@link ISelectionProvider}:
+ * when the selection changes in the canvas, it is thus broadcasted to anyone listening
+ * on the site's selection service.
+ * <p/>
+ * This part is also an {@link ISelectionListener}. It listens to the site's selection
+ * service and thus receives selection changes from itself as well as the associated
+ * outline and property sheet (these are registered by {@link LayoutEditorDelegate#delegateGetAdapter(Class)}).
+ *
+ * @since GLE2
+ */
+public class GraphicalEditorPart extends EditorPart
+ implements IPageImageProvider, INullSelectionListener, IFlyoutListener,
+ ConfigurationClient {
+
+ /*
+ * Useful notes:
+ * To understand Drag & drop:
+ * http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html
+ *
+ * To understand the site's selection listener, selection provider, and the
+ * confusion of different-yet-similarly-named interfaces, consult this:
+ * http://www.eclipse.org/articles/Article-WorkbenchSelections/article.html
+ *
+ * To summarize the selection mechanism:
+ * - The workbench site selection service can be seen as "centralized"
+ * service that registers selection providers and selection listeners.
+ * - The editor part and the outline are selection providers.
+ * - The editor part, the outline and the property sheet are listeners
+ * which all listen to each others indirectly.
+ */
+
+ /** Property key for the window preferences for the structure flyout */
+ private static final String PREF_STRUCTURE = "design.structure"; //$NON-NLS-1$
+
+ /** Property key for the window preferences for the palette flyout */
+ private static final String PREF_PALETTE = "design.palette"; //$NON-NLS-1$
+
+ /**
+ * Session-property on files which specifies the initial config state to be used on
+ * this file
+ */
+ public final static QualifiedName NAME_INITIAL_STATE =
+ new QualifiedName(AdtPlugin.PLUGIN_ID, "initialstate");//$NON-NLS-1$
+
+ /**
+ * Session-property on files which specifies the inclusion-context (reference to another layout
+ * which should be "including" this layout) when the file is opened
+ */
+ public final static QualifiedName NAME_INCLUDE =
+ new QualifiedName(AdtPlugin.PLUGIN_ID, "includer");//$NON-NLS-1$
+
+ /** Reference to the layout editor */
+ private final LayoutEditorDelegate mEditorDelegate;
+
+ /** Reference to the file being edited. Can also be used to access the {@link IProject}. */
+ private IFile mEditedFile;
+
+ /** The configuration chooser at the top of the layout editor. */
+ private ConfigurationChooser mConfigChooser;
+
+ /** The sash that splits the palette from the error view.
+ * The error view is shown only when needed. */
+ private SashForm mSashError;
+
+ /** The palette displayed on the left of the sash. */
+ private PaletteControl mPalette;
+
+ /** The layout canvas displayed to the right of the sash. */
+ private LayoutCanvasViewer mCanvasViewer;
+
+ /** The Rules Engine associated with this editor. It is project-specific. */
+ private RulesEngine mRulesEngine;
+
+ /** Styled text displaying the most recent error in the error view. */
+ private StyledText mErrorLabel;
+
+ /**
+ * The resource reference to a file that should surround this file (e.g. include this file
+ * visually), or null if not applicable
+ */
+ private Reference mIncludedWithin;
+
+ private Map<ResourceType, Map<String, ResourceValue>> mConfiguredFrameworkRes;
+ private Map<ResourceType, Map<String, ResourceValue>> mConfiguredProjectRes;
+ private ProjectCallback mProjectCallback;
+ private boolean mNeedsRecompute = false;
+ private TargetListener mTargetListener;
+ private ResourceResolver mResourceResolver;
+ private ReloadListener mReloadListener;
+ private int mMinSdkVersion;
+ private int mTargetSdkVersion;
+ private LayoutActionBar mActionBar;
+ private OutlinePage mOutlinePage;
+ private FlyoutControlComposite mStructureFlyout;
+ private FlyoutControlComposite mPaletteComposite;
+ private PropertyFactory mPropertyFactory;
+ private boolean mRenderedOnce;
+ private final Object mCredential = new Object();
+
+ /**
+ * Flags which tracks whether this editor is currently active which is set whenever
+ * {@link #activated()} is called and clear whenever {@link #deactivated()} is called.
+ * This is used to suppress repeated calls to {@link #activate()} to avoid doing
+ * unnecessary work.
+ */
+ private boolean mActive;
+
+ /**
+ * Constructs a new {@link GraphicalEditorPart}
+ *
+ * @param editorDelegate the associated XML editor delegate
+ */
+ public GraphicalEditorPart(@NonNull LayoutEditorDelegate editorDelegate) {
+ mEditorDelegate = editorDelegate;
+ setPartName("Graphical Layout");
+ }
+
+ // ------------------------------------
+ // Methods overridden from base classes
+ //------------------------------------
+
+ /**
+ * Initializes the editor part with a site and input.
+ * {@inheritDoc}
+ */
+ @Override
+ public void init(IEditorSite site, IEditorInput input) throws PartInitException {
+ setSite(site);
+ useNewEditorInput(input);
+
+ if (mTargetListener == null) {
+ mTargetListener = new TargetListener();
+ AdtPlugin.getDefault().addTargetListener(mTargetListener);
+
+ // Trigger a check to see if the SDK needs to be reloaded (which will
+ // invoke onSdkLoaded asynchronously as needed).
+ AdtPlugin.getDefault().refreshSdk();
+ }
+ }
+
+ private void useNewEditorInput(IEditorInput input) throws PartInitException {
+ // The contract of init() mentions we need to fail if we can't understand the input.
+ if (!(input instanceof FileEditorInput)) {
+ throw new PartInitException("Input is not of type FileEditorInput: " + //$NON-NLS-1$
+ input == null ? "null" : input.toString()); //$NON-NLS-1$
+ }
+ }
+
+ @Override
+ public Image getPageImage() {
+ return IconFactory.getInstance().getIcon("editor_page_design"); //$NON-NLS-1$
+ }
+
+ @Override
+ public void createPartControl(Composite parent) {
+
+ Display d = parent.getDisplay();
+
+ GridLayout gl = new GridLayout(1, false);
+ parent.setLayout(gl);
+ gl.marginHeight = gl.marginWidth = 0;
+
+ // Check whether somebody has requested an initial state for the newly opened file.
+ // The initial state is a serialized version of the state compatible with
+ // {@link ConfigurationComposite#CONFIG_STATE}.
+ String initialState = null;
+ IFile file = mEditedFile;
+ if (file == null) {
+ IEditorInput input = mEditorDelegate.getEditor().getEditorInput();
+ if (input instanceof FileEditorInput) {
+ file = ((FileEditorInput) input).getFile();
+ }
+ }
+
+ if (file != null) {
+ try {
+ initialState = (String) file.getSessionProperty(NAME_INITIAL_STATE);
+ if (initialState != null) {
+ // Only use once
+ file.setSessionProperty(NAME_INITIAL_STATE, null);
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't read session property %1$s", NAME_INITIAL_STATE);
+ }
+ }
+
+ IPreferenceStore preferenceStore = AdtPlugin.getDefault().getPreferenceStore();
+ PluginFlyoutPreferences preferences;
+ preferences = new PluginFlyoutPreferences(preferenceStore, PREF_PALETTE);
+ preferences.initializeDefaults(DOCK_WEST, STATE_OPEN, 200);
+ mPaletteComposite = new FlyoutControlComposite(parent, SWT.NONE, preferences);
+ mPaletteComposite.setTitleText("Palette");
+ mPaletteComposite.setMinWidth(100);
+ Composite paletteParent = mPaletteComposite.getFlyoutParent();
+ Composite editorParent = mPaletteComposite.getClientParent();
+ mPaletteComposite.setListener(this);
+
+ mPaletteComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ PageSiteComposite paletteComposite = new PageSiteComposite(paletteParent, SWT.BORDER);
+ paletteComposite.setTitleText("Palette");
+ paletteComposite.setTitleImage(IconFactory.getInstance().getIcon("palette"));
+ PalettePage decor = new PalettePage(this);
+ paletteComposite.setPage(decor);
+ mPalette = (PaletteControl) decor.getControl();
+ decor.createToolbarItems(paletteComposite.getToolBar());
+
+ // Create the shared structure+editor area
+ preferences = new PluginFlyoutPreferences(preferenceStore, PREF_STRUCTURE);
+ preferences.initializeDefaults(DOCK_EAST, STATE_OPEN, 300);
+ mStructureFlyout = new FlyoutControlComposite(editorParent, SWT.NONE, preferences);
+ mStructureFlyout.setTitleText("Structure");
+ mStructureFlyout.setMinWidth(150);
+ mStructureFlyout.setListener(this);
+
+ Composite layoutBarAndCanvas = new Composite(mStructureFlyout.getClientParent(), SWT.NONE);
+ GridLayout gridLayout = new GridLayout(1, false);
+ gridLayout.horizontalSpacing = 0;
+ gridLayout.verticalSpacing = 0;
+ gridLayout.marginWidth = 0;
+ gridLayout.marginHeight = 0;
+ layoutBarAndCanvas.setLayout(gridLayout);
+
+ mConfigChooser = new ConfigurationChooser(this, layoutBarAndCanvas, initialState);
+ mConfigChooser.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mActionBar = new LayoutActionBar(layoutBarAndCanvas, SWT.NONE, this);
+ GridData detailsData = new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1);
+ mActionBar.setLayoutData(detailsData);
+ if (file != null) {
+ mActionBar.updateErrorIndicator(file);
+ }
+
+ mSashError = new SashForm(layoutBarAndCanvas, SWT.VERTICAL | SWT.BORDER);
+ mSashError.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ mCanvasViewer = new LayoutCanvasViewer(mEditorDelegate, mRulesEngine, mSashError, SWT.NONE);
+ mSashError.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
+
+ mErrorLabel = new StyledText(mSashError, SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL);
+ mErrorLabel.setEditable(false);
+ mErrorLabel.setBackground(d.getSystemColor(SWT.COLOR_INFO_BACKGROUND));
+ mErrorLabel.setForeground(d.getSystemColor(SWT.COLOR_INFO_FOREGROUND));
+ mErrorLabel.addMouseListener(new ErrorLabelListener());
+
+ mSashError.setWeights(new int[] { 80, 20 });
+ mSashError.setMaximizedControl(mCanvasViewer.getControl());
+
+ // Create the structure views. We really should do this *lazily*, but that
+ // seems to cause a bug: property sheet won't update. Track this down later.
+ createStructureViews(mStructureFlyout.getFlyoutParent(), false);
+ showStructureViews(false, false, false);
+
+ // Initialize the state
+ reloadPalette();
+
+ IWorkbenchPartSite site = getSite();
+ site.setSelectionProvider(mCanvasViewer);
+ site.getPage().addSelectionListener(this);
+ }
+
+ private void createStructureViews(Composite parent, boolean createPropertySheet) {
+ mOutlinePage = new OutlinePage(this);
+ mOutlinePage.setShowPropertySheet(createPropertySheet);
+ mOutlinePage.setShowHeader(true);
+
+ IPageSite pageSite = new IPageSite() {
+
+ @Override
+ public IWorkbenchPage getPage() {
+ return getSite().getPage();
+ }
+
+ @Override
+ public ISelectionProvider getSelectionProvider() {
+ return getSite().getSelectionProvider();
+ }
+
+ @Override
+ public Shell getShell() {
+ return getSite().getShell();
+ }
+
+ @Override
+ public IWorkbenchWindow getWorkbenchWindow() {
+ return getSite().getWorkbenchWindow();
+ }
+
+ @Override
+ public void setSelectionProvider(ISelectionProvider provider) {
+ getSite().setSelectionProvider(provider);
+ }
+
+ @Override
+ public Object getAdapter(Class adapter) {
+ return getSite().getAdapter(adapter);
+ }
+
+ @Override
+ public Object getService(Class api) {
+ return getSite().getService(api);
+ }
+
+ @Override
+ public boolean hasService(Class api) {
+ return getSite().hasService(api);
+ }
+
+ @Override
+ public void registerContextMenu(String menuId, MenuManager menuManager,
+ ISelectionProvider selectionProvider) {
+ }
+
+ @Override
+ public IActionBars getActionBars() {
+ return null;
+ }
+ };
+ mOutlinePage.init(pageSite);
+ mOutlinePage.createControl(parent);
+ mOutlinePage.addSelectionChangedListener(new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ getCanvasControl().getSelectionManager().setSelection(event.getSelection());
+ }
+ });
+ }
+
+ /** Shows the embedded (within the layout editor) outline and or properties */
+ void showStructureViews(final boolean showOutline, final boolean showProperties,
+ final boolean updateLayout) {
+ Display display = mConfigChooser.getDisplay();
+ if (display.getThread() != Thread.currentThread()) {
+ display.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (!mConfigChooser.isDisposed()) {
+ showStructureViews(showOutline, showProperties, updateLayout);
+ }
+ }
+
+ });
+ return;
+ }
+
+ boolean show = showOutline || showProperties;
+
+ Control[] children = mStructureFlyout.getFlyoutParent().getChildren();
+ if (children.length == 0) {
+ if (show) {
+ createStructureViews(mStructureFlyout.getFlyoutParent(), showProperties);
+ }
+ return;
+ }
+
+ mOutlinePage.setShowPropertySheet(showProperties);
+
+ Control control = children[0];
+ if (show != control.getVisible()) {
+ control.setVisible(show);
+ mOutlinePage.setActive(show); // disable/re-enable listeners etc
+ if (show) {
+ ISelection selection = getCanvasControl().getSelectionManager().getSelection();
+ mOutlinePage.selectionChanged(getEditorDelegate().getEditor(), selection);
+ }
+ if (updateLayout) {
+ mStructureFlyout.layout();
+ }
+ // TODO: *dispose* the non-showing widgets to save memory?
+ }
+ }
+
+ /**
+ * Returns the property factory associated with this editor
+ *
+ * @return the factory
+ */
+ @NonNull
+ public PropertyFactory getPropertyFactory() {
+ if (mPropertyFactory == null) {
+ mPropertyFactory = new PropertyFactory(this);
+ }
+
+ return mPropertyFactory;
+ }
+
+ /**
+ * Invoked by {@link LayoutCanvas} to set the model (a.k.a. the root view info).
+ *
+ * @param rootViewInfo The root of the view info hierarchy. Can be null.
+ */
+ public void setModel(CanvasViewInfo rootViewInfo) {
+ if (mOutlinePage != null) {
+ mOutlinePage.setModel(rootViewInfo);
+ }
+ }
+
+ /**
+ * Listens to workbench selections that does NOT come from {@link LayoutEditorDelegate}
+ * (those are generated by ourselves).
+ * <p/>
+ * Selection can be null, as indicated by this class implementing
+ * {@link INullSelectionListener}.
+ */
+ @Override
+ public void selectionChanged(IWorkbenchPart part, ISelection selection) {
+ Object delegate = part instanceof IEditorPart ?
+ LayoutEditorDelegate.fromEditor((IEditorPart) part) : null;
+ if (delegate == null) {
+ if (part instanceof PageBookView) {
+ PageBookView pbv = (PageBookView) part;
+ org.eclipse.ui.part.IPage currentPage = pbv.getCurrentPage();
+ if (currentPage instanceof OutlinePage) {
+ LayoutCanvas canvas = getCanvasControl();
+ if (canvas != null && canvas.getOutlinePage() != currentPage) {
+ // The notification is not for this view; ignore
+ // (can happen when there are multiple pages simultaneously
+ // visible)
+ return;
+ }
+ }
+ }
+ mCanvasViewer.setSelection(selection);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ getSite().getPage().removeSelectionListener(this);
+ getSite().setSelectionProvider(null);
+
+ if (mTargetListener != null) {
+ AdtPlugin.getDefault().removeTargetListener(mTargetListener);
+ mTargetListener = null;
+ }
+
+ if (mReloadListener != null) {
+ LayoutReloadMonitor.getMonitor().removeListener(mReloadListener);
+ mReloadListener = null;
+ }
+
+ if (mCanvasViewer != null) {
+ mCanvasViewer.dispose();
+ mCanvasViewer = null;
+ }
+ super.dispose();
+ }
+
+ /**
+ * Select the visual element corresponding to the given XML node
+ * @param xmlNode The Node whose element we want to select
+ */
+ public void select(Node xmlNode) {
+ mCanvasViewer.getCanvas().getSelectionManager().select(xmlNode);
+ }
+
+ // ---- Implements ConfigurationClient ----
+ @Override
+ public void aboutToChange(int flags) {
+ if ((flags & CFG_TARGET) != 0) {
+ IAndroidTarget oldTarget = mConfigChooser.getConfiguration().getTarget();
+ preRenderingTargetChangeCleanUp(oldTarget);
+ }
+ }
+
+ @Override
+ public boolean changed(int flags) {
+ mConfiguredFrameworkRes = mConfiguredProjectRes = null;
+ mResourceResolver = null;
+
+ if (mEditedFile == null) {
+ return true;
+ }
+
+ // Before doing the normal process, test for the following case.
+ // - the editor is being opened (or reset for a new input)
+ // - the file being opened is not the best match for any possible configuration
+ // - another random compatible config was chosen in the config composite.
+ // The result is that 'match' will not be the file being edited, but because this is not
+ // due to a config change, we should not trigger opening the actual best match (also,
+ // because the editor is still opening the MatchingStrategy woudln't answer true
+ // and the best match file would open in a different editor).
+ // So the solution is that if the editor is being created, we just call recomputeLayout
+ // without looking for a better matching layout file.
+ if (mEditorDelegate.getEditor().isCreatingPages()) {
+ recomputeLayout();
+ } else {
+ boolean affectsFileSelection = (flags & Configuration.MASK_FILE_ATTRS) != 0;
+ IFile best = null;
+ // get the resources of the file's project.
+ if (affectsFileSelection) {
+ best = ConfigurationMatcher.getBestFileMatch(mConfigChooser);
+ }
+ if (best != null) {
+ if (!best.equals(mEditedFile)) {
+ try {
+ // tell the editor that the next replacement file is due to a config
+ // change.
+ mEditorDelegate.setNewFileOnConfigChange(true);
+
+ boolean reuseEditor = AdtPrefs.getPrefs().isSharedLayoutEditor();
+ if (!reuseEditor) {
+ String data = ConfigurationDescription.getDescription(best);
+ if (data == null) {
+ // Not previously opened: duplicate the current state as
+ // much as possible
+ data = mConfigChooser.getConfiguration().toPersistentString();
+ ConfigurationDescription.setDescription(best, data);
+ }
+ }
+
+ // ask the IDE to open the replacement file.
+ IDE.openEditor(getSite().getWorkbenchWindow().getActivePage(), best,
+ CommonXmlEditor.ID);
+
+ // we're done!
+ return reuseEditor;
+ } catch (PartInitException e) {
+ // FIXME: do something!
+ }
+ }
+
+ // at this point, we have not opened a new file.
+
+ // Store the state in the current file
+ mConfigChooser.saveConstraints();
+
+ // Even though the layout doesn't change, the config changed, and referenced
+ // resources need to be updated.
+ recomputeLayout();
+ } else if (affectsFileSelection) {
+ // display the error.
+ Configuration configuration = mConfigChooser.getConfiguration();
+ FolderConfiguration currentConfig = configuration.getFullConfig();
+ displayError(
+ "No resources match the configuration\n" +
+ " \n" +
+ "\t%1$s\n" +
+ " \n" +
+ "Change the configuration or create:\n" +
+ " \n" +
+ "\tres/%2$s/%3$s\n" +
+ " \n" +
+ "You can also click the 'Create New...' item in the configuration " +
+ "dropdown menu above.",
+ currentConfig.toDisplayString(),
+ currentConfig.getFolderName(ResourceFolderType.LAYOUT),
+ mEditedFile.getName());
+ } else {
+ // Something else changed, such as the theme - just recompute existing
+ // layout
+ mConfigChooser.saveConstraints();
+ recomputeLayout();
+ }
+ }
+
+ if ((flags & CFG_TARGET) != 0) {
+ Configuration configuration = mConfigChooser.getConfiguration();
+ IAndroidTarget target = configuration.getTarget();
+ Sdk current = Sdk.getCurrent();
+ if (current != null) {
+ AndroidTargetData targetData = current.getTargetData(target);
+ updateCapabilities(targetData);
+ }
+ }
+
+ if ((flags & (CFG_DEVICE | CFG_DEVICE_STATE)) != 0) {
+ // When the device changes, zoom the view to fit, but only up to 100% (e.g. zoom
+ // out to fit the content, or zoom back in if we were zoomed out more from the
+ // previous view, but only up to 100% such that we never blow up pixels
+ if (mActionBar.isZoomingAllowed()) {
+ getCanvasControl().setFitScale(true, true /*allowZoomIn*/);
+ }
+ }
+
+ reloadPalette();
+
+ getCanvasControl().getPreviewManager().configurationChanged(flags);
+
+ return true;
+ }
+
+ @Override
+ public void setActivity(@NonNull String activity) {
+ ManifestInfo manifest = ManifestInfo.get(mEditedFile.getProject());
+ String pkg = manifest.getPackage();
+ if (activity.startsWith(pkg) && activity.length() > pkg.length()
+ && activity.charAt(pkg.length()) == '.') {
+ activity = activity.substring(pkg.length());
+ }
+ CommonXmlEditor editor = getEditorDelegate().getEditor();
+ Element element = editor.getUiRootNode().getXmlDocument().getDocumentElement();
+ AdtUtils.setToolsAttribute(editor,
+ element, "Choose Activity", ATTR_CONTEXT,
+ activity, false /*reveal*/, false /*append*/);
+ }
+
+ /**
+ * Returns a {@link ProjectResources} for the framework resources based on the current
+ * configuration selection.
+ * @return the framework resources or null if not found.
+ */
+ @Override
+ @Nullable
+ public ResourceRepository getFrameworkResources() {
+ return getFrameworkResources(getRenderingTarget());
+ }
+
+ /**
+ * Returns a {@link ProjectResources} for the framework resources of a given
+ * target.
+ * @param target the target for which to return the framework resources.
+ * @return the framework resources or null if not found.
+ */
+ @Override
+ @Nullable
+ public ResourceRepository getFrameworkResources(@Nullable IAndroidTarget target) {
+ if (target != null) {
+ AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
+
+ if (data != null) {
+ return data.getFrameworkResources();
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public ProjectResources getProjectResources() {
+ if (mEditedFile != null) {
+ ResourceManager manager = ResourceManager.getInstance();
+ return manager.getProjectResources(mEditedFile.getProject());
+ }
+
+ return null;
+ }
+
+
+ @Override
+ @NonNull
+ public Map<ResourceType, Map<String, ResourceValue>> getConfiguredFrameworkResources() {
+ if (mConfiguredFrameworkRes == null && mConfigChooser != null) {
+ ResourceRepository frameworkRes = getFrameworkResources();
+
+ if (frameworkRes == null) {
+ AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework");
+ } else {
+ // get the framework resource values based on the current config
+ mConfiguredFrameworkRes = frameworkRes.getConfiguredResources(
+ mConfigChooser.getConfiguration().getFullConfig());
+ }
+ }
+
+ return mConfiguredFrameworkRes;
+ }
+
+ @Override
+ @NonNull
+ public Map<ResourceType, Map<String, ResourceValue>> getConfiguredProjectResources() {
+ if (mConfiguredProjectRes == null && mConfigChooser != null) {
+ ProjectResources project = getProjectResources();
+
+ // get the project resource values based on the current config
+ mConfiguredProjectRes = project.getConfiguredResources(
+ mConfigChooser.getConfiguration().getFullConfig());
+ }
+
+ return mConfiguredProjectRes;
+ }
+
+ @Override
+ public void createConfigFile() {
+ LayoutCreatorDialog dialog = new LayoutCreatorDialog(mConfigChooser.getShell(),
+ mEditedFile.getName(), mConfigChooser.getConfiguration().getFullConfig());
+ if (dialog.open() != Window.OK) {
+ return;
+ }
+
+ FolderConfiguration config = new FolderConfiguration();
+ dialog.getConfiguration(config);
+
+ // Creates a new layout file from the specified {@link FolderConfiguration}.
+ CreateNewConfigJob job = new CreateNewConfigJob(this, mEditedFile, config);
+ job.schedule();
+ }
+
+ /**
+ * Returns the resource name of the file that is including this current layout, if any
+ * (may be null)
+ *
+ * @return the resource name of an including layout, or null
+ */
+ @Override
+ public Reference getIncludedWithin() {
+ return mIncludedWithin;
+ }
+
+ @Override
+ @Nullable
+ public LayoutCanvas getCanvas() {
+ return getCanvasControl();
+ }
+
+ /**
+ * Listens to target changed in the current project, to trigger a new layout rendering.
+ */
+ private class TargetListener implements ITargetChangeListener {
+
+ @Override
+ public void onProjectTargetChange(IProject changedProject) {
+ if (changedProject != null && changedProject.equals(getProject())) {
+ updateEditor();
+ }
+ }
+
+ @Override
+ public void onTargetLoaded(IAndroidTarget loadedTarget) {
+ IAndroidTarget target = getRenderingTarget();
+ if (target != null && target.equals(loadedTarget)) {
+ updateEditor();
+ }
+ }
+
+ @Override
+ public void onSdkLoaded() {
+ // get the current rendering target to unload it
+ IAndroidTarget oldTarget = getRenderingTarget();
+ preRenderingTargetChangeCleanUp(oldTarget);
+
+ computeSdkVersion();
+
+ // get the project target
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
+ if (target != null) {
+ mConfigChooser.onSdkLoaded(target);
+ changed(CFG_FOLDER | CFG_TARGET);
+ }
+ }
+ }
+
+ private void updateEditor() {
+ mEditorDelegate.getEditor().commitPages(false /* onSave */);
+
+ // because the target changed we must reset the configured resources.
+ mConfiguredFrameworkRes = mConfiguredProjectRes = null;
+ mResourceResolver = null;
+
+ // make sure we remove the custom view loader, since its parent class loader is the
+ // bridge class loader.
+ mProjectCallback = null;
+
+ // recreate the ui root node always, this will also call onTargetChange
+ // on the config composite
+ mEditorDelegate.delegateInitUiRootNode(true /*force*/);
+ }
+
+ private IProject getProject() {
+ return getEditorDelegate().getEditor().getProject();
+ }
+ }
+
+ /** Refresh the configured project resources associated with this editor */
+ public void refreshProjectResources() {
+ mConfiguredProjectRes = null;
+ mResourceResolver = null;
+ }
+
+ /**
+ * Returns the currently edited file
+ *
+ * @return the currently edited file, or null
+ */
+ public IFile getEditedFile() {
+ return mEditedFile;
+ }
+
+ /**
+ * Returns the project for the currently edited file, or null
+ *
+ * @return the project containing the edited file, or null
+ */
+ public IProject getProject() {
+ if (mEditedFile != null) {
+ return mEditedFile.getProject();
+ } else {
+ return null;
+ }
+ }
+
+ // ----------------
+
+ /**
+ * Save operation in the Graphical Editor Part.
+ * <p/>
+ * In our workflow, the model is owned by the Structured XML Editor.
+ * The graphical layout editor just displays it -- thus we don't really
+ * save anything here.
+ * <p/>
+ * This must NOT call the parent editor part. At the contrary, the parent editor
+ * part will call this *after* having done the actual save operation.
+ * <p/>
+ * The only action this editor must do is mark the undo command stack as
+ * being no longer dirty.
+ */
+ @Override
+ public void doSave(IProgressMonitor monitor) {
+ // TODO implement a command stack
+// getCommandStack().markSaveLocation();
+// firePropertyChange(PROP_DIRTY);
+ }
+
+ /**
+ * Save operation in the Graphical Editor Part.
+ * <p/>
+ * In our workflow, the model is owned by the Structured XML Editor.
+ * The graphical layout editor just displays it -- thus we don't really
+ * save anything here.
+ */
+ @Override
+ public void doSaveAs() {
+ // pass
+ }
+
+ /**
+ * In our workflow, the model is owned by the Structured XML Editor.
+ * The graphical layout editor just displays it -- thus we don't really
+ * save anything here.
+ */
+ @Override
+ public boolean isDirty() {
+ return false;
+ }
+
+ /**
+ * In our workflow, the model is owned by the Structured XML Editor.
+ * The graphical layout editor just displays it -- thus we don't really
+ * save anything here.
+ */
+ @Override
+ public boolean isSaveAsAllowed() {
+ return false;
+ }
+
+ @Override
+ public void setFocus() {
+ // TODO Auto-generated method stub
+
+ }
+
+ /**
+ * Responds to a page change that made the Graphical editor page the activated page.
+ */
+ public void activated() {
+ if (!mActive) {
+ mActive = true;
+
+ syncDockingState();
+ mActionBar.updateErrorIndicator();
+
+ boolean changed = mConfigChooser.syncRenderState();
+ if (changed) {
+ // Will also force recomputeLayout()
+ return;
+ }
+
+ if (mNeedsRecompute) {
+ recomputeLayout();
+ }
+
+ mCanvasViewer.getCanvas().syncPreviewMode();
+ }
+ }
+
+ /**
+ * The global docking state version. This number is incremented each time
+ * the user customizes the window layout in any layout.
+ */
+ private static int sDockingStateVersion;
+
+ /**
+ * The window docking state version that this window is currently showing;
+ * when a different window is reconfigured, the global version number is
+ * incremented, and when this window is shown, and the current version is
+ * less than the global version, the window layout will be synced.
+ */
+ private int mDockingStateVersion;
+
+ /**
+ * Syncs the window docking state.
+ * <p>
+ * The layout editor lets you change the docking state -- e.g. you can minimize the
+ * palette, and drag the structure view to the bottom, and so on. When you restart
+ * the IDE, the window comes back up with your customized state.
+ * <p>
+ * <b>However</b>, when you have multiple editor files open, if you minimize the palette
+ * in one editor and then switch to another, the other editor will have the old window
+ * state. That's because each editor has its own set of windows.
+ * <p>
+ * This method fixes this. Whenever a window is shown, this method is called, and the
+ * docking state is synced such that the editor will match the current persistent docking
+ * state.
+ */
+ private void syncDockingState() {
+ if (mDockingStateVersion == sDockingStateVersion) {
+ // No changes to apply
+ return;
+ }
+ mDockingStateVersion = sDockingStateVersion;
+
+ IPreferenceStore preferenceStore = AdtPlugin.getDefault().getPreferenceStore();
+ PluginFlyoutPreferences preferences;
+ preferences = new PluginFlyoutPreferences(preferenceStore, PREF_PALETTE);
+ mPaletteComposite.apply(preferences);
+ preferences = new PluginFlyoutPreferences(preferenceStore, PREF_STRUCTURE);
+ mStructureFlyout.apply(preferences);
+ mPaletteComposite.layout();
+ mStructureFlyout.layout();
+ mPaletteComposite.redraw(); // the structure view is nested within the palette
+ }
+
+ /**
+ * Responds to a page change that made the Graphical editor page the deactivated page
+ */
+ public void deactivated() {
+ mActive = false;
+
+ LayoutCanvas canvas = getCanvasControl();
+ if (canvas != null) {
+ canvas.deactivated();
+ }
+ }
+
+ /**
+ * Opens and initialize the editor with a new file.
+ * @param file the file being edited.
+ */
+ public void openFile(IFile file) {
+ mEditedFile = file;
+ mConfigChooser.setFile(mEditedFile);
+
+ if (mReloadListener == null) {
+ mReloadListener = new ReloadListener();
+ LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), mReloadListener);
+ }
+
+ if (mRulesEngine == null) {
+ mRulesEngine = new RulesEngine(this, mEditedFile.getProject());
+ if (mCanvasViewer != null) {
+ mCanvasViewer.getCanvas().setRulesEngine(mRulesEngine);
+ }
+ }
+
+ // Pick up hand-off data: somebody requesting this file to be opened may have
+ // requested that it should be opened as included within another file
+ if (mEditedFile != null) {
+ try {
+ mIncludedWithin = (Reference) mEditedFile.getSessionProperty(NAME_INCLUDE);
+ if (mIncludedWithin != null) {
+ // Only use once
+ mEditedFile.setSessionProperty(NAME_INCLUDE, null);
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't access session property %1$s", NAME_INCLUDE);
+ }
+ }
+
+ computeSdkVersion();
+ }
+
+ /**
+ * Resets the editor with a replacement file.
+ * @param file the replacement file.
+ */
+ public void replaceFile(IFile file) {
+ mEditedFile = file;
+ mConfigChooser.replaceFile(mEditedFile);
+ computeSdkVersion();
+ }
+
+ /**
+ * Resets the editor with a replacement file coming from a config change in the config
+ * selector.
+ * @param file the replacement file.
+ */
+ public void changeFileOnNewConfig(IFile file) {
+ mEditedFile = file;
+ mConfigChooser.changeFileOnNewConfig(mEditedFile);
+ }
+
+ /**
+ * Responds to a target change for the project of the edited file
+ */
+ public void onTargetChange() {
+ AndroidTargetData targetData = mConfigChooser.onXmlModelLoaded();
+ updateCapabilities(targetData);
+
+ changed(CFG_FOLDER | CFG_TARGET);
+ }
+
+ /** Updates the capabilities for the given target data (which may be null) */
+ private void updateCapabilities(AndroidTargetData targetData) {
+ if (targetData != null) {
+ LayoutLibrary layoutLib = targetData.getLayoutLibrary();
+ if (mIncludedWithin != null && !layoutLib.supports(Capability.EMBEDDED_LAYOUT)) {
+ showIn(null);
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link CommonXmlDelegate} for this editor
+ *
+ * @return the {@link CommonXmlDelegate} for this editor
+ */
+ @NonNull
+ public LayoutEditorDelegate getEditorDelegate() {
+ return mEditorDelegate;
+ }
+
+ /**
+ * Returns the {@link RulesEngine} associated with this editor
+ *
+ * @return the {@link RulesEngine} associated with this editor, never null
+ */
+ public RulesEngine getRulesEngine() {
+ return mRulesEngine;
+ }
+
+ /**
+ * Return the {@link LayoutCanvas} associated with this editor
+ *
+ * @return the associated {@link LayoutCanvas}
+ */
+ public LayoutCanvas getCanvasControl() {
+ if (mCanvasViewer != null) {
+ return mCanvasViewer.getCanvas();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link UiDocumentNode} for the XML model edited by this editor
+ *
+ * @return the associated model
+ */
+ public UiDocumentNode getModel() {
+ return mEditorDelegate.getUiRootNode();
+ }
+
+ /**
+ * Callback for XML model changed. Only update/recompute the layout if the editor is visible
+ */
+ public void onXmlModelChanged() {
+ // To optimize the rendering when the user is editing in the XML pane, we don't
+ // refresh the editor if it's not the active part.
+ //
+ // This behavior is acceptable when the editor is the single "full screen" part
+ // (as in this case active means visible.)
+ // Unfortunately this breaks in 2 cases:
+ // - when performing a drag'n'drop from one editor to another, the target is not
+ // properly refreshed before it becomes active.
+ // - when duplicating the editor window and placing both editors side by side (xml in one
+ // and canvas in the other one), the canvas may not be refreshed when the XML is edited.
+ //
+ // TODO find a way to really query whether the pane is visible, not just active.
+
+ if (mEditorDelegate.isGraphicalEditorActive()) {
+ recomputeLayout();
+ } else {
+ // Remember we want to recompute as soon as the editor becomes active.
+ mNeedsRecompute = true;
+ }
+ }
+
+ /**
+ * Recomputes the layout
+ */
+ public void recomputeLayout() {
+ try {
+ if (!ensureFileValid()) {
+ return;
+ }
+
+ UiDocumentNode model = getModel();
+ LayoutCanvas canvas = mCanvasViewer.getCanvas();
+ if (!ensureModelValid(model)) {
+ // Although we display an error, we still treat an empty document as a
+ // successful layout result so that we can drop new elements in it.
+ //
+ // For that purpose, create a special LayoutScene that has no image,
+ // no root view yet indicates success and then update the canvas with it.
+
+ canvas.setSession(
+ new StaticRenderSession(
+ Result.Status.SUCCESS.createResult(),
+ null /*rootViewInfo*/, null /*image*/),
+ null /*explodeNodes*/, true /* layoutlib5 */);
+ return;
+ }
+
+ LayoutLibrary layoutLib = getReadyLayoutLib(true /*displayError*/);
+
+ if (layoutLib != null) {
+ // if drawing in real size, (re)set the scaling factor.
+ if (mActionBar.isZoomingRealSize()) {
+ mActionBar.computeAndSetRealScale(false /* redraw */);
+ }
+
+ IProject project = mEditedFile.getProject();
+ renderWithBridge(project, model, layoutLib);
+
+ canvas.getPreviewManager().renderPreviews();
+ }
+ } finally {
+ // no matter the result, we are done doing the recompute based on the latest
+ // resource/code change.
+ mNeedsRecompute = false;
+ }
+ }
+
+ /**
+ * Reloads the palette
+ */
+ public void reloadPalette() {
+ if (mPalette != null) {
+ IAndroidTarget renderingTarget = getRenderingTarget();
+ if (renderingTarget != null) {
+ mPalette.reloadPalette(renderingTarget);
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link LayoutLibrary} associated with this editor, if it has
+ * been initialized already. May return null if it has not been initialized (or has
+ * not finished initializing).
+ *
+ * @return The {@link LayoutLibrary}, or null
+ */
+ public LayoutLibrary getLayoutLibrary() {
+ return getReadyLayoutLib(false /*displayError*/);
+ }
+
+ /**
+ * Returns the scale to multiply pixels in the layout coordinate space with to obtain
+ * the corresponding dip (device independent pixel)
+ *
+ * @return the scale to multiple layout coordinates with to obtain the dip position
+ */
+ public float getDipScale() {
+ float dpi = mConfigChooser.getConfiguration().getDensity().getDpiValue();
+ return Density.DEFAULT_DENSITY / dpi;
+ }
+
+ // --- private methods ---
+
+ /**
+ * Ensure that the file associated with this editor is valid (exists and is
+ * synchronized). Any reasons why it is not are displayed in the editor's error area.
+ *
+ * @return True if the editor is valid, false otherwise.
+ */
+ private boolean ensureFileValid() {
+ // check that the resource exists. If the file is opened but the project is closed
+ // or deleted for some reason (changed from outside of eclipse), then this will
+ // return false;
+ if (mEditedFile.exists() == false) {
+ displayError("Resource '%1$s' does not exist.",
+ mEditedFile.getFullPath().toString());
+ return false;
+ }
+
+ if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) {
+ String message = String.format("%1$s is out of sync. Please refresh.",
+ mEditedFile.getName());
+
+ displayError(message);
+
+ // also print it in the error console.
+ IProject iProject = mEditedFile.getProject();
+ AdtPlugin.printErrorToConsole(iProject.getName(), message);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns a {@link LayoutLibrary} that is ready for rendering, or null if the bridge
+ * is not available or not ready yet (due to SDK loading still being in progress etc).
+ * If enabled, any reasons preventing the bridge from being returned are displayed to the
+ * editor's error area.
+ *
+ * @param displayError whether to display the loading error or not.
+ *
+ * @return LayoutBridge the layout bridge for rendering this editor's scene
+ */
+ LayoutLibrary getReadyLayoutLib(boolean displayError) {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = getRenderingTarget();
+
+ if (target != null) {
+ AndroidTargetData data = currentSdk.getTargetData(target);
+ if (data != null) {
+ LayoutLibrary layoutLib = data.getLayoutLibrary();
+
+ if (layoutLib.getStatus() == LoadStatus.LOADED) {
+ return layoutLib;
+ } else if (displayError) { // getBridge() == null
+ // SDK is loaded but not the layout library!
+
+ // check whether the bridge managed to load, or not
+ if (layoutLib.getStatus() == LoadStatus.LOADING) {
+ displayError("Eclipse is loading framework information and the layout library from the SDK folder.\n%1$s will refresh automatically once the process is finished.",
+ mEditedFile.getName());
+ } else {
+ String message = layoutLib.getLoadMessage();
+ displayError("Eclipse failed to load the framework information and the layout library!" +
+ message != null ? "\n" + message : "");
+ }
+ }
+ } else { // data == null
+ // It can happen that the workspace refreshes while the SDK is loading its
+ // data, which could trigger a redraw of the opened layout if some resources
+ // changed while Eclipse is closed.
+ // In this case data could be null, but this is not an error.
+ // We can just silently return, as all the opened editors are automatically
+ // refreshed once the SDK finishes loading.
+ LoadStatus targetLoadStatus = currentSdk.checkAndLoadTargetData(target, null);
+
+ // display error is asked.
+ if (displayError) {
+ String targetName = target.getName();
+ switch (targetLoadStatus) {
+ case LOADING:
+ String s;
+ if (currentSdk.getTarget(getProject()) == target) {
+ s = String.format(
+ "The project target (%1$s) is still loading.",
+ targetName);
+ } else {
+ s = String.format(
+ "The rendering target (%1$s) is still loading.",
+ targetName);
+ }
+ s += "\nThe layout will refresh automatically once the process is finished.";
+ displayError(s);
+
+ break;
+ case FAILED: // known failure
+ case LOADED: // success but data isn't loaded?!?!
+ displayError("The project target (%s) was not properly loaded.",
+ targetName);
+ break;
+ }
+ }
+ }
+
+ } else if (displayError) { // target == null
+ displayError("The project target is not set. Right click project, choose Properties | Android.");
+ }
+ } else if (displayError) { // currentSdk == null
+ displayError("Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.",
+ mEditedFile.getName());
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link IAndroidTarget} used for the rendering.
+ * <p/>
+ * This first looks for the rendering target setup in the config UI, and if nothing has
+ * been setup yet, returns the target of the project.
+ *
+ * @return an IAndroidTarget object or null if no target is setup and the project has no
+ * target set.
+ *
+ */
+ public IAndroidTarget getRenderingTarget() {
+ // if the SDK is null no targets are loaded.
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk == null) {
+ return null;
+ }
+
+ // attempt to get a target from the configuration selector.
+ IAndroidTarget renderingTarget = mConfigChooser.getConfiguration().getTarget();
+ if (renderingTarget != null) {
+ return renderingTarget;
+ }
+
+ // fall back to the project target
+ if (mEditedFile != null) {
+ return currentSdk.getTarget(mEditedFile.getProject());
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns whether the current rendering target supports the given capability
+ *
+ * @param capability the capability to be looked up
+ * @return true if the current rendering target supports the given capability
+ */
+ public boolean renderingSupports(Capability capability) {
+ IAndroidTarget target = getRenderingTarget();
+ if (target != null) {
+ AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target);
+ LayoutLibrary layoutLib = targetData.getLayoutLibrary();
+ return layoutLib.supports(capability);
+ }
+
+ return false;
+ }
+
+ private boolean ensureModelValid(UiDocumentNode model) {
+ // check there is actually a model (maybe the file is empty).
+ if (model.getUiChildren().size() == 0) {
+ if (mEditorDelegate.getEditor().isCreatingPages()) {
+ displayError("Loading editor");
+ return false;
+ }
+ displayError(
+ "No XML content. Please add a root view or layout to your document.");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Creates a {@link RenderService} associated with this editor
+ * @return the render service
+ */
+ @NonNull
+ public RenderService createRenderService() {
+ return RenderService.create(this, mCredential);
+ }
+
+ /**
+ * Creates a {@link RenderLogger} associated with this editor
+ * @param name the name of the logger
+ * @return the new logger
+ */
+ @NonNull
+ public RenderLogger createRenderLogger(String name) {
+ return new RenderLogger(name, mCredential);
+ }
+
+ /**
+ * Creates a {@link RenderService} associated with this editor
+ *
+ * @param configuration the configuration to use (and fallback to editor for the rest)
+ * @param resolver a resource resolver to use to look up resources
+ * @return the render service
+ */
+ @NonNull
+ public RenderService createRenderService(Configuration configuration,
+ ResourceResolver resolver) {
+ return RenderService.create(this, configuration, resolver, mCredential);
+ }
+
+ private void renderWithBridge(IProject iProject, UiDocumentNode model,
+ LayoutLibrary layoutLib) {
+ LayoutCanvas canvas = getCanvasControl();
+ Set<UiElementNode> explodeNodes = canvas.getNodesToExplode();
+ RenderLogger logger = createRenderLogger(mEditedFile.getName());
+ RenderingMode renderingMode = RenderingMode.NORMAL;
+ // FIXME set the rendering mode using ViewRule or something.
+ List<UiElementNode> children = model.getUiChildren();
+ if (children.size() > 0 &&
+ children.get(0).getDescriptor().getXmlLocalName().equals(SCROLL_VIEW)) {
+ renderingMode = RenderingMode.V_SCROLL;
+ }
+
+ RenderSession session = RenderService.create(this, mCredential)
+ .setModel(model)
+ .setLog(logger)
+ .setRenderingMode(renderingMode)
+ .setIncludedWithin(mIncludedWithin)
+ .setNodesToExpand(explodeNodes)
+ .createRenderSession();
+
+ boolean layoutlib5 = layoutLib.supports(Capability.EMBEDDED_LAYOUT);
+ canvas.setSession(session, explodeNodes, layoutlib5);
+
+ // update the UiElementNode with the layout info.
+ if (session != null && session.getResult().isSuccess() == false) {
+ // An error was generated. Print it (and any other accumulated warnings)
+ String errorMessage = session.getResult().getErrorMessage();
+ Throwable exception = session.getResult().getException();
+ if (exception != null && errorMessage == null) {
+ errorMessage = exception.toString();
+ }
+ if (exception != null || (errorMessage != null && errorMessage.length() > 0)) {
+ logger.error(null, errorMessage, exception, null /*data*/);
+ } else if (!logger.hasProblems()) {
+ logger.error(null, "Unexpected error in rendering, no details given",
+ null /*data*/);
+ }
+ // These errors will be included in the log warnings which are
+ // displayed regardless of render success status below
+ }
+
+ // We might have detected some missing classes and swapped them by a mock view,
+ // or run into fidelity warnings or missing resources, so emit all these
+ // warnings
+ Set<String> missingClasses = mProjectCallback.getMissingClasses();
+ Set<String> brokenClasses = mProjectCallback.getUninstantiatableClasses();
+ if (logger.hasProblems()) {
+ displayLoggerProblems(iProject, logger);
+ displayFailingClasses(missingClasses, brokenClasses, true);
+ displayUserStackTrace(logger, true);
+ } else if (missingClasses.size() > 0 || brokenClasses.size() > 0) {
+ displayFailingClasses(missingClasses, brokenClasses, false);
+ displayUserStackTrace(logger, true);
+ } else if (session != null) {
+ // Nope, no missing or broken classes. Clear success, congrats!
+ hideError();
+
+ // First time this layout is opened, run lint on the file (after a delay)
+ if (!mRenderedOnce) {
+ mRenderedOnce = true;
+ Job job = new Job("Run Lint") {
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ getEditorDelegate().delegateRunLint();
+ return Status.OK_STATUS;
+ }
+
+ };
+ job.setSystem(true);
+ job.schedule(3000); // 3 seconds
+ }
+
+ mConfigChooser.ensureInitialized();
+ }
+
+ model.refreshUi();
+ }
+
+ /**
+ * Returns the {@link ResourceResolver} for this editor
+ *
+ * @return the resolver used to resolve resources for the current configuration of
+ * this editor, or null
+ */
+ public ResourceResolver getResourceResolver() {
+ if (mResourceResolver == null) {
+ String theme = mConfigChooser.getThemeName();
+ if (theme == null) {
+ displayError("Missing theme.");
+ return null;
+ }
+ boolean isProjectTheme = mConfigChooser.getConfiguration().isProjectTheme();
+
+ Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes =
+ getConfiguredProjectResources();
+
+ // Get the framework resources
+ Map<ResourceType, Map<String, ResourceValue>> frameworkResources =
+ getConfiguredFrameworkResources();
+
+ if (configuredProjectRes == null) {
+ displayError("Missing project resources for current configuration.");
+ return null;
+ }
+
+ if (frameworkResources == null) {
+ displayError("Missing framework resources.");
+ return null;
+ }
+
+ mResourceResolver = ResourceResolver.create(
+ configuredProjectRes, frameworkResources,
+ theme, isProjectTheme);
+ }
+
+ return mResourceResolver;
+ }
+
+ /** Returns a project callback, and optionally resets it */
+ ProjectCallback getProjectCallback(boolean reset, LayoutLibrary layoutLibrary) {
+ // Lazily create the project callback the first time we need it
+ if (mProjectCallback == null) {
+ ResourceManager resManager = ResourceManager.getInstance();
+ IProject project = getProject();
+ ProjectResources projectRes = resManager.getProjectResources(project);
+ mProjectCallback = new ProjectCallback(layoutLibrary, projectRes, project,
+ mCredential, this);
+ } else if (reset) {
+ // Also clears the set of missing/broken classes prior to rendering
+ mProjectCallback.getMissingClasses().clear();
+ mProjectCallback.getUninstantiatableClasses().clear();
+ }
+
+ return mProjectCallback;
+ }
+
+ /**
+ * Returns the resource name of this layout, NOT including the @layout/ prefix
+ *
+ * @return the resource name of this layout, NOT including the @layout/ prefix
+ */
+ public String getLayoutResourceName() {
+ return ResourceHelper.getLayoutName(mEditedFile);
+ }
+
+ /**
+ * Cleans up when the rendering target is about to change
+ * @param oldTarget the old rendering target.
+ */
+ private void preRenderingTargetChangeCleanUp(IAndroidTarget oldTarget) {
+ // first clear the caches related to this file in the old target
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ AndroidTargetData data = currentSdk.getTargetData(oldTarget);
+ if (data != null) {
+ LayoutLibrary layoutLib = data.getLayoutLibrary();
+
+ // layoutLib can never be null.
+ layoutLib.clearCaches(mEditedFile.getProject());
+ }
+ }
+
+ // Also remove the ProjectCallback as it caches custom views which must be reloaded
+ // with the classloader of the new LayoutLib. We also have to clear it out
+ // because it stores a reference to the layout library which could have changed.
+ mProjectCallback = null;
+
+ // FIXME: get rid of the current LayoutScene if any.
+ }
+
+ private class ReloadListener implements ILayoutReloadListener {
+ /**
+ * Called when the file changes triggered a redraw of the layout
+ */
+ @Override
+ public void reloadLayout(final ChangeFlags flags, final boolean libraryChanged) {
+ if (mConfigChooser.isDisposed()) {
+ return;
+ }
+ Display display = mConfigChooser.getDisplay();
+ display.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ reloadLayoutSwt(flags, libraryChanged);
+ }
+ });
+ }
+
+ /** Reload layout. <b>Must be called on the SWT thread</b> */
+ private void reloadLayoutSwt(ChangeFlags flags, boolean libraryChanged) {
+ if (mConfigChooser.isDisposed()) {
+ return;
+ }
+ assert mConfigChooser.getDisplay().getThread() == Thread.currentThread();
+
+ boolean recompute = false;
+ // we only care about the r class of the main project.
+ if (flags.rClass && libraryChanged == false) {
+ recompute = true;
+ if (mEditedFile != null) {
+ ResourceManager manager = ResourceManager.getInstance();
+ ProjectResources projectRes = manager.getProjectResources(
+ mEditedFile.getProject());
+
+ if (projectRes != null) {
+ projectRes.resetDynamicIds();
+ }
+ }
+ }
+
+ if (flags.localeList) {
+ // the locale list *potentially* changed so we update the locale in the
+ // config composite.
+ // However there's no recompute, as it could not be needed
+ // (for instance a new layout)
+ // If a resource that's not a layout changed this will trigger a recompute anyway.
+ mConfigChooser.updateLocales();
+ }
+
+ // if a resources was modified.
+ if (flags.resources) {
+ recompute = true;
+
+ // TODO: differentiate between single and multi resource file changed, and whether
+ // the resource change affects the cache.
+
+ // force a reparse in case a value XML file changed.
+ mConfiguredProjectRes = null;
+ mResourceResolver = null;
+
+ // clear the cache in the bridge in case a bitmap/9-patch changed.
+ LayoutLibrary layoutLib = getReadyLayoutLib(true /*displayError*/);
+ if (layoutLib != null) {
+ layoutLib.clearCaches(mEditedFile.getProject());
+ }
+ }
+
+ if (flags.code) {
+ // only recompute if the custom view loader was used to load some code.
+ if (mProjectCallback != null && mProjectCallback.isUsed()) {
+ mProjectCallback = null;
+ recompute = true;
+ }
+ }
+
+ if (flags.manifest) {
+ recompute |= computeSdkVersion();
+ }
+
+ if (recompute) {
+ if (mEditorDelegate.isGraphicalEditorActive()) {
+ recomputeLayout();
+ } else {
+ mNeedsRecompute = true;
+ }
+ }
+ }
+ }
+
+ // ---- Error handling ----
+
+ /**
+ * Switches the sash to display the error label.
+ *
+ * @param errorFormat The new error to display if not null.
+ * @param parameters String.format parameters for the error format.
+ */
+ private void displayError(String errorFormat, Object...parameters) {
+ if (errorFormat != null) {
+ mErrorLabel.setText(String.format(errorFormat, parameters));
+ } else {
+ mErrorLabel.setText("");
+ }
+ mSashError.setMaximizedControl(null);
+ }
+
+ /** Displays the canvas and hides the error label. */
+ private void hideError() {
+ mErrorLabel.setText("");
+ mSashError.setMaximizedControl(mCanvasViewer.getControl());
+ }
+
+ /** Display the problem list encountered during a render */
+ private void displayUserStackTrace(RenderLogger logger, boolean append) {
+ List<Throwable> throwables = logger.getFirstTrace();
+ if (throwables == null || throwables.isEmpty()) {
+ return;
+ }
+
+ Throwable throwable = throwables.get(0);
+
+ if (throwable instanceof RenderSecurityException) {
+ addActionLink(mErrorLabel, ActionLinkStyleRange.LINK_DISABLE_SANDBOX,
+ "\nTurn off custom view rendering sandbox\n");
+
+ StringBuilder builder = new StringBuilder(200);
+ String lastFailedPath = RenderSecurityManager.getLastFailedPath();
+ if (lastFailedPath != null) {
+ builder.append("Diagnostic info for ADT bug report:\n");
+ builder.append("Failed path: ").append(lastFailedPath).append('\n');
+ String tempDir = System.getProperty("java.io.tmpdir");
+ builder.append("Normal temp dir: ").append(tempDir).append('\n');
+ File normalized = new File(tempDir);
+ builder.append("Normalized temp dir: ").append(normalized.getPath()).append('\n');
+ try {
+ builder.append("Canonical temp dir: ").append(normalized.getCanonicalPath())
+ .append('\n');
+ } catch (IOException e) {
+ // ignore
+ }
+ builder.append("os.name: ").append(System.getProperty("os.name")).append('\n');
+ builder.append("os.version: ").append(System.getProperty("os.version"));
+ builder.append('\n');
+ builder.append("java.runtime.version: ");
+ builder.append(System.getProperty("java.runtime.version"));
+ }
+ if (throwable.getMessage().equals("Unable to create temporary file")) {
+ String javaVersion = System.getProperty("java.version");
+ if (javaVersion.startsWith("1.7.0_")) {
+ int version = Integer
+ .parseInt(javaVersion.substring(javaVersion.indexOf('_') + 1));
+ if (version > 0 && version < 45) {
+ builder.append('\n');
+ builder.append("Tip: This may be caused by using an older version " +
+ "of JDK 1.7.0; try using at least 1.7.0_45 (you are using " +
+ javaVersion + ")");
+ }
+ }
+ }
+ if (builder.length() > 0) {
+ addText(mErrorLabel, builder.toString());
+ }
+ }
+
+ StackTraceElement[] frames = throwable.getStackTrace();
+ int end = -1;
+ boolean haveInterestingFrame = false;
+ for (int i = 0; i < frames.length; i++) {
+ StackTraceElement frame = frames[i];
+ if (isInterestingFrame(frame)) {
+ haveInterestingFrame = true;
+ }
+ String className = frame.getClassName();
+ if (className.equals(
+ "com.android.layoutlib.bridge.impl.RenderSessionImpl")) { //$NON-NLS-1$
+ end = i;
+ break;
+ }
+ }
+
+ if (end == -1 || !haveInterestingFrame) {
+ // Not a recognized stack trace range: just skip it
+ return;
+ }
+
+ if (!append) {
+ mErrorLabel.setText("\n"); //$NON-NLS-1$
+ } else {
+ addText(mErrorLabel, "\n\n"); //$NON-NLS-1$
+ }
+
+ addText(mErrorLabel, throwable.toString() + '\n');
+ for (int i = 0; i < end; i++) {
+ StackTraceElement frame = frames[i];
+ String className = frame.getClassName();
+ String methodName = frame.getMethodName();
+ addText(mErrorLabel, " at " + className + '.' + methodName + '(');
+ String fileName = frame.getFileName();
+ if (fileName != null && !fileName.isEmpty()) {
+ int lineNumber = frame.getLineNumber();
+ String location = fileName + ':' + lineNumber;
+ if (isInterestingFrame(frame)) {
+ addActionLink(mErrorLabel, ActionLinkStyleRange.LINK_OPEN_LINE,
+ location, className, methodName, fileName, lineNumber);
+ } else {
+ addText(mErrorLabel, location);
+ }
+ addText(mErrorLabel, ")\n"); //$NON-NLS-1$
+ }
+ }
+ }
+
+ private static boolean isInterestingFrame(StackTraceElement frame) {
+ String className = frame.getClassName();
+ return !(className.startsWith("android.") //$NON-NLS-1$
+ || className.startsWith("com.android.") //$NON-NLS-1$
+ || className.startsWith("java.") //$NON-NLS-1$
+ || className.startsWith("javax.") //$NON-NLS-1$
+ || className.startsWith("sun.")); //$NON-NLS-1$
+ }
+
+ /**
+ * Switches the sash to display the error label to show a list of
+ * missing classes and give options to create them.
+ */
+ private void displayFailingClasses(Set<String> missingClasses, Set<String> brokenClasses,
+ boolean append) {
+ if (missingClasses.size() == 0 && brokenClasses.size() == 0) {
+ return;
+ }
+
+ if (!append) {
+ mErrorLabel.setText(""); //$NON-NLS-1$
+ } else {
+ addText(mErrorLabel, "\n"); //$NON-NLS-1$
+ }
+
+ if (missingClasses.size() > 0) {
+ addText(mErrorLabel, "The following classes could not be found:\n");
+ for (String clazz : missingClasses) {
+ addText(mErrorLabel, "- ");
+ addText(mErrorLabel, clazz);
+ addText(mErrorLabel, " (");
+
+ IProject project = getProject();
+ Collection<String> customViews = getCustomViewClassNames(project);
+ addTypoSuggestions(clazz, customViews, false);
+ addTypoSuggestions(clazz, customViews, true);
+ addTypoSuggestions(clazz, getAndroidViewClassNames(project), false);
+
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.LINK_FIX_BUILD_PATH, "Fix Build Path", clazz);
+ addText(mErrorLabel, ", ");
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.LINK_EDIT_XML, "Edit XML", clazz);
+ if (clazz.indexOf('.') != -1) {
+ // Add "Create Class" link, but only for custom views
+ addText(mErrorLabel, ", ");
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.LINK_CREATE_CLASS, "Create Class", clazz);
+ }
+ addText(mErrorLabel, ")\n");
+ }
+ }
+ if (brokenClasses.size() > 0) {
+ addText(mErrorLabel, "The following classes could not be instantiated:\n");
+
+ // Do we have a custom class (not an Android or add-ons class)
+ boolean haveCustomClass = false;
+
+ for (String clazz : brokenClasses) {
+ addText(mErrorLabel, "- ");
+ addText(mErrorLabel, clazz);
+ addText(mErrorLabel, " (");
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.LINK_OPEN_CLASS, "Open Class", clazz);
+ addText(mErrorLabel, ", ");
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.LINK_SHOW_LOG, "Show Error Log", clazz);
+ addText(mErrorLabel, ")\n");
+
+ if (!(clazz.startsWith("android.") || //$NON-NLS-1$
+ clazz.startsWith("com.google."))) { //$NON-NLS-1$
+ haveCustomClass = true;
+ }
+ }
+
+ addText(mErrorLabel, "See the Error Log (Window > Show View) for more details.\n");
+
+ if (haveCustomClass) {
+ addBoldText(mErrorLabel, "Tip: Use View.isInEditMode() in your custom views "
+ + "to skip code when shown in Eclipse");
+ }
+ }
+
+ mSashError.setMaximizedControl(null);
+ }
+
+ private void addTypoSuggestions(String actual, Collection<String> views,
+ boolean compareWithPackage) {
+ if (views.size() == 0) {
+ return;
+ }
+
+ // Look for typos and try to match with custom views and android views
+ String actualBase = actual.substring(actual.lastIndexOf('.') + 1);
+ int maxDistance = actualBase.length() >= 4 ? 2 : 1;
+
+ if (views.size() > 0) {
+ for (String suggested : views) {
+ String suggestedBase = suggested.substring(suggested.lastIndexOf('.') + 1);
+
+ String matchWith = compareWithPackage ? suggested : suggestedBase;
+ if (Math.abs(actualBase.length() - matchWith.length()) > maxDistance) {
+ // The string lengths differ more than the allowed edit distance;
+ // no point in even attempting to compute the edit distance (requires
+ // O(n*m) storage and O(n*m) speed, where n and m are the string lengths)
+ continue;
+ }
+ if (LintUtils.editDistance(actualBase, matchWith) <= maxDistance) {
+ // Suggest this class as a typo for the given class
+ String labelClass = (suggestedBase.equals(actual) || actual.indexOf('.') != -1)
+ ? suggested : suggestedBase;
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.LINK_CHANGE_CLASS_TO,
+ String.format("Change to %1$s",
+ // Only show full package name if class name
+ // is the same
+ labelClass),
+ actual,
+ viewNeedsPackage(suggested) ? suggested : suggestedBase);
+ addText(mErrorLabel, ", ");
+ }
+ }
+ }
+ }
+
+ private static Collection<String> getCustomViewClassNames(IProject project) {
+ CustomViewFinder finder = CustomViewFinder.get(project);
+ Collection<String> views = finder.getAllViews();
+ if (views == null) {
+ finder.refresh();
+ views = finder.getAllViews();
+ }
+
+ return views;
+ }
+
+ private static Collection<String> getAndroidViewClassNames(IProject project) {
+ Sdk currentSdk = Sdk.getCurrent();
+ IAndroidTarget target = currentSdk.getTarget(project);
+ if (target != null) {
+ AndroidTargetData targetData = currentSdk.getTargetData(target);
+ if (targetData != null) {
+ LayoutDescriptors layoutDescriptors = targetData.getLayoutDescriptors();
+ return layoutDescriptors.getAllViewClassNames();
+ }
+ }
+
+ return Collections.emptyList();
+ }
+
+ /** Add a normal line of text to the styled text widget. */
+ private void addText(StyledText styledText, String...string) {
+ for (String s : string) {
+ styledText.append(s);
+ }
+ }
+
+ /** Display the problem list encountered during a render */
+ private void displayLoggerProblems(IProject project, RenderLogger logger) {
+ if (logger.hasProblems()) {
+ mErrorLabel.setText("");
+ // A common source of problems is attempting to open a layout when there are
+ // compilation errors. In this case, may not have run (or may not be up to date)
+ // so resources cannot be looked up etc. Explain this situation to the user.
+
+ boolean hasAaptErrors = false;
+ boolean hasJavaErrors = false;
+ try {
+ IMarker[] markers;
+ markers = project.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE);
+ if (markers.length > 0) {
+ for (IMarker marker : markers) {
+ String markerType = marker.getType();
+ if (markerType.equals(IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER)) {
+ int severity = marker.getAttribute(IMarker.SEVERITY, -1);
+ if (severity == IMarker.SEVERITY_ERROR) {
+ hasJavaErrors = true;
+ }
+ } else if (markerType.equals(AdtConstants.MARKER_AAPT_COMPILE)) {
+ int severity = marker.getAttribute(IMarker.SEVERITY, -1);
+ if (severity == IMarker.SEVERITY_ERROR) {
+ hasAaptErrors = true;
+ }
+ }
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ if (logger.seenTagPrefix(LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR)) {
+ addBoldText(mErrorLabel,
+ "Missing styles. Is the correct theme chosen for this layout?\n");
+ addText(mErrorLabel,
+ "Use the Theme combo box above the layout to choose a different layout, " +
+ "or fix the theme style references.\n\n");
+ }
+
+ List<Throwable> trace = logger.getFirstTrace();
+ if (trace != null
+ && trace.toString().contains(
+ "java.lang.IndexOutOfBoundsException: Index: 2, Size: 2") //$NON-NLS-1$
+ && mConfigChooser.getConfiguration().getDensity() == Density.TV) {
+ addBoldText(mErrorLabel,
+ "It looks like you are using a render target where the layout library " +
+ "does not support the tvdpi density.\n\n");
+ addText(mErrorLabel, "Please try either updating to " +
+ "the latest available version (using the SDK manager), or if no updated " +
+ "version is available for this specific version of Android, try using " +
+ "a more recent render target version.\n\n");
+
+ }
+
+ if (hasAaptErrors && logger.seenTagPrefix(LayoutLog.TAG_RESOURCES_PREFIX)) {
+ // Text will automatically be wrapped by the error widget so no reason
+ // to insert linebreaks in this error message:
+ String message =
+ "NOTE: This project contains resource errors, so aapt did not succeed, "
+ + "which can cause rendering failures. "
+ + "Fix resource problems first.\n\n";
+ addBoldText(mErrorLabel, message);
+ } else if (hasJavaErrors && mProjectCallback != null && mProjectCallback.isUsed()) {
+ // Text will automatically be wrapped by the error widget so no reason
+ // to insert linebreaks in this error message:
+ String message =
+ "NOTE: This project contains Java compilation errors, "
+ + "which can cause rendering failures for custom views. "
+ + "Fix compilation problems first.\n\n";
+ addBoldText(mErrorLabel, message);
+ }
+
+ if (logger.seenTag(RenderLogger.TAG_MISSING_DIMENSION)) {
+ List<UiElementNode> elements = UiDocumentNode.getAllElements(getModel());
+ for (UiElementNode element : elements) {
+ String width = element.getAttributeValue(ATTR_LAYOUT_WIDTH);
+ if (width == null || width.length() == 0) {
+ addSetAttributeLink(element, ATTR_LAYOUT_WIDTH);
+ }
+
+ String height = element.getAttributeValue(ATTR_LAYOUT_HEIGHT);
+ if (height == null || height.length() == 0) {
+ addSetAttributeLink(element, ATTR_LAYOUT_HEIGHT);
+ }
+ }
+ }
+
+ String problems = logger.getProblems(false /*includeFidelityWarnings*/);
+ addText(mErrorLabel, problems);
+
+ List<String> fidelityWarnings = logger.getFidelityWarnings();
+ if (fidelityWarnings != null && fidelityWarnings.size() > 0) {
+ addText(mErrorLabel,
+ "The graphics preview in the layout editor may not be accurate:\n");
+ for (String warning : fidelityWarnings) {
+ addText(mErrorLabel, warning + ' ');
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.IGNORE_FIDELITY_WARNING,
+ "(Ignore for this session)\n", warning);
+ }
+ }
+
+ mSashError.setMaximizedControl(null);
+ } else {
+ mSashError.setMaximizedControl(mCanvasViewer.getControl());
+ }
+ }
+
+ /** Appends an action link to set the given attribute on the given value */
+ private void addSetAttributeLink(UiElementNode element, String attribute) {
+ if (element.getXmlNode().getNodeName().equals(GRID_LAYOUT)) {
+ // GridLayout does not require a layout_width or layout_height to be defined
+ return;
+ }
+
+ String fill = VALUE_FILL_PARENT;
+ // See whether we should offer match_parent instead of fill_parent
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(getProject());
+ if (target.getVersion().getApiLevel() >= 8) {
+ fill = VALUE_MATCH_PARENT;
+ }
+ }
+
+ String id = element.getAttributeValue(ATTR_ID);
+ if (id == null || id.length() == 0) {
+ id = '<' + element.getXmlNode().getNodeName() + '>';
+ } else {
+ id = BaseLayoutRule.stripIdPrefix(id);
+ }
+
+ addText(mErrorLabel, String.format("\"%1$s\" does not set the required %2$s attribute:\n",
+ id, attribute));
+ addText(mErrorLabel, " (1) ");
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.SET_ATTRIBUTE,
+ String.format("Set to \"%1$s\"", VALUE_WRAP_CONTENT),
+ element, attribute, VALUE_WRAP_CONTENT);
+ addText(mErrorLabel, "\n (2) ");
+ addActionLink(mErrorLabel,
+ ActionLinkStyleRange.SET_ATTRIBUTE,
+ String.format("Set to \"%1$s\"\n", fill),
+ element, attribute, fill);
+ }
+
+ /** Appends the given text as a bold string in the given text widget */
+ private void addBoldText(StyledText styledText, String text) {
+ String s = styledText.getText();
+ int start = (s == null ? 0 : s.length());
+
+ styledText.append(text);
+ StyleRange sr = new StyleRange();
+ sr.start = start;
+ sr.length = text.length();
+ sr.fontStyle = SWT.BOLD;
+ styledText.setStyleRange(sr);
+ }
+
+ /**
+ * Add a URL-looking link to the styled text widget.
+ * <p/>
+ * A mouse-click listener is setup and it interprets the link based on the
+ * action, corresponding to the value fields in {@link ActionLinkStyleRange}.
+ */
+ private void addActionLink(StyledText styledText, int action, String label,
+ Object... data) {
+ String s = styledText.getText();
+ int start = (s == null ? 0 : s.length());
+ styledText.append(label);
+
+ StyleRange sr = new ActionLinkStyleRange(action, data);
+ sr.start = start;
+ sr.length = label.length();
+ sr.fontStyle = SWT.NORMAL;
+ sr.underlineStyle = SWT.UNDERLINE_LINK;
+ sr.underline = true;
+ styledText.setStyleRange(sr);
+ }
+
+ /**
+ * Looks up the resource file corresponding to the given type
+ *
+ * @param type The type of resource to look up, such as {@link ResourceType#LAYOUT}
+ * @param name The name of the resource (not including ".xml")
+ * @param isFrameworkResource if true, the resource is a framework resource, otherwise
+ * it's a project resource
+ * @return the resource file defining the named resource, or null if not found
+ */
+ public IPath findResourceFile(ResourceType type, String name, boolean isFrameworkResource) {
+ // FIXME: This code does not handle theme value resolution.
+ // There is code to handle this, but it's in layoutlib; we should
+ // expose that and use it here.
+
+ Map<ResourceType, Map<String, ResourceValue>> map;
+ map = isFrameworkResource ? mConfiguredFrameworkRes : mConfiguredProjectRes;
+ if (map == null) {
+ // Not yet configured
+ return null;
+ }
+
+ Map<String, ResourceValue> layoutMap = map.get(type);
+ if (layoutMap != null) {
+ ResourceValue value = layoutMap.get(name);
+ if (value != null) {
+ String valueStr = value.getValue();
+ if (valueStr.startsWith("?")) { //$NON-NLS-1$
+ // FIXME: It's a reference. We should resolve this properly.
+ return null;
+ }
+ return new Path(valueStr);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Looks up the path to the file corresponding to the given attribute value, such as
+ * @layout/foo, which will return the foo.xml file in res/layout/. (The general format
+ * of the resource url is {@literal @[<package_name>:]<resource_type>/<resource_name>}.
+ *
+ * @param url the attribute url
+ * @return the path to the file defining this attribute, or null if not found
+ */
+ public IPath findResourceFile(String url) {
+ if (!url.startsWith("@")) { //$NON-NLS-1$
+ return null;
+ }
+ int typeEnd = url.indexOf('/', 1);
+ if (typeEnd == -1) {
+ return null;
+ }
+ int nameBegin = typeEnd + 1;
+ int typeBegin = 1;
+ int colon = url.lastIndexOf(':', typeEnd);
+ boolean isFrameworkResource = false;
+ if (colon != -1) {
+ // The URL contains a package name.
+ // While the url format technically allows other package names,
+ // the platform apparently only supports @android for now (or if it does,
+ // there are no usages in the current code base so this is not common).
+ String packageName = url.substring(typeBegin, colon);
+ if (ANDROID_PKG.equals(packageName)) {
+ isFrameworkResource = true;
+ }
+
+ typeBegin = colon + 1;
+ }
+
+ String typeName = url.substring(typeBegin, typeEnd);
+ ResourceType type = ResourceType.getEnum(typeName);
+ if (type == null) {
+ return null;
+ }
+
+ String name = url.substring(nameBegin);
+ return findResourceFile(type, name, isFrameworkResource);
+ }
+
+ /**
+ * Resolve the given @string reference into a literal String using the current project
+ * configuration
+ *
+ * @param text the text resource reference to resolve
+ * @return the resolved string, or null
+ */
+ public String findString(String text) {
+ if (text.startsWith(STRING_PREFIX)) {
+ return findString(text.substring(STRING_PREFIX.length()), false);
+ } else if (text.startsWith(ANDROID_STRING_PREFIX)) {
+ return findString(text.substring(ANDROID_STRING_PREFIX.length()), true);
+ } else {
+ return text;
+ }
+ }
+
+ private String findString(String name, boolean isFrameworkResource) {
+ Map<ResourceType, Map<String, ResourceValue>> map;
+ map = isFrameworkResource ? mConfiguredFrameworkRes : mConfiguredProjectRes;
+ if (map == null) {
+ // Not yet configured
+ return null;
+ }
+
+ Map<String, ResourceValue> layoutMap = map.get(ResourceType.STRING);
+ if (layoutMap != null) {
+ ResourceValue value = layoutMap.get(name);
+ if (value != null) {
+ // FIXME: This code does not handle theme value resolution.
+ // There is code to handle this, but it's in layoutlib; we should
+ // expose that and use it here.
+ return value.getValue();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * This StyleRange represents a clickable link in the render output, where various
+ * actions can be taken such as creating a class, opening the project chooser to
+ * adjust the build path, etc.
+ */
+ private class ActionLinkStyleRange extends StyleRange {
+ /** Create a view class */
+ private static final int LINK_CREATE_CLASS = 1;
+ /** Edit the build path for the current project */
+ private static final int LINK_FIX_BUILD_PATH = 2;
+ /** Show the XML tab */
+ private static final int LINK_EDIT_XML = 3;
+ /** Open the given class */
+ private static final int LINK_OPEN_CLASS = 4;
+ /** Show the error log */
+ private static final int LINK_SHOW_LOG = 5;
+ /** Change the class reference to the given fully qualified name */
+ private static final int LINK_CHANGE_CLASS_TO = 6;
+ /** Ignore the given fidelity warning */
+ private static final int IGNORE_FIDELITY_WARNING = 7;
+ /** Set an attribute on the given XML element to a given value */
+ private static final int SET_ATTRIBUTE = 8;
+ /** Open the given file and line number */
+ private static final int LINK_OPEN_LINE = 9;
+ /** Disable sandbox */
+ private static final int LINK_DISABLE_SANDBOX = 10;
+
+ /** Client data: the contents depend on the specific action */
+ private final Object[] mData;
+ /** The action to be taken when the link is clicked */
+ private final int mAction;
+
+ private ActionLinkStyleRange(int action, Object... data) {
+ super();
+ mAction = action;
+ mData = data;
+ }
+
+ /** Performs the click action */
+ public void onClick() {
+ switch (mAction) {
+ case LINK_CREATE_CLASS:
+ createNewClass((String) mData[0]);
+ break;
+ case LINK_EDIT_XML:
+ mEditorDelegate.getEditor().setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID);
+ break;
+ case LINK_FIX_BUILD_PATH:
+ @SuppressWarnings("restriction")
+ String id = BuildPathsPropertyPage.PROP_ID;
+ PreferencesUtil.createPropertyDialogOn(
+ AdtPlugin.getShell(),
+ getProject(), id, null, null).open();
+ break;
+ case LINK_OPEN_CLASS:
+ AdtPlugin.openJavaClass(getProject(), (String) mData[0]);
+ break;
+ case LINK_OPEN_LINE:
+ boolean success = AdtPlugin.openStackTraceLine(
+ (String) mData[0], // class
+ (String) mData[1], // method
+ (String) mData[2], // file
+ (Integer) mData[3]); // line
+ if (!success) {
+ MessageDialog.openError(mErrorLabel.getShell(), "Not Found",
+ String.format("Could not find %1$s.%2$s", mData[0], mData[1]));
+ }
+ break;
+ case LINK_SHOW_LOG:
+ IWorkbench workbench = PlatformUI.getWorkbench();
+ IWorkbenchWindow workbenchWindow = workbench.getActiveWorkbenchWindow();
+ try {
+ IWorkbenchPage page = workbenchWindow.getActivePage();
+ page.showView("org.eclipse.pde.runtime.LogView"); //$NON-NLS-1$
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, null);
+ }
+ break;
+ case LINK_CHANGE_CLASS_TO:
+ // Change class reference of mData[0] to mData[1]
+ // TODO: run under undo lock
+ MultiTextEdit edits = new MultiTextEdit();
+ ISourceViewer textViewer =
+ mEditorDelegate.getEditor().getStructuredSourceViewer();
+ IDocument document = textViewer.getDocument();
+ String xml = document.get();
+ int index = 0;
+ // Replace <old with <new and </old with </new
+ String prefix = "<"; //$NON-NLS-1$
+ String find = prefix + mData[0];
+ String replaceWith = prefix + mData[1];
+ while (true) {
+ index = xml.indexOf(find, index);
+ if (index == -1) {
+ break;
+ }
+ edits.addChild(new ReplaceEdit(index, find.length(), replaceWith));
+ index += find.length();
+ }
+ index = 0;
+ prefix = "</"; //$NON-NLS-1$
+ find = prefix + mData[0];
+ replaceWith = prefix + mData[1];
+ while (true) {
+ index = xml.indexOf(find, index);
+ if (index == -1) {
+ break;
+ }
+ edits.addChild(new ReplaceEdit(index, find.length(), replaceWith));
+ index += find.length();
+ }
+ // Handle <view class="old">
+ index = 0;
+ prefix = "\""; //$NON-NLS-1$
+ String suffix = "\""; //$NON-NLS-1$
+ find = prefix + mData[0] + suffix;
+ replaceWith = prefix + mData[1] + suffix;
+ while (true) {
+ index = xml.indexOf(find, index);
+ if (index == -1) {
+ break;
+ }
+ edits.addChild(new ReplaceEdit(index, find.length(), replaceWith));
+ index += find.length();
+ }
+ try {
+ edits.apply(document);
+ } catch (MalformedTreeException e) {
+ AdtPlugin.log(e, null);
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+ break;
+ case IGNORE_FIDELITY_WARNING:
+ RenderLogger.ignoreFidelityWarning((String) mData[0]);
+ recomputeLayout();
+ break;
+ case SET_ATTRIBUTE: {
+ final UiElementNode element = (UiElementNode) mData[0];
+ final String attribute = (String) mData[1];
+ final String value = (String) mData[2];
+ mEditorDelegate.getEditor().wrapUndoEditXmlModel(
+ String.format("Set \"%1$s\" to \"%2$s\"", attribute, value),
+ new Runnable() {
+ @Override
+ public void run() {
+ element.setAttributeValue(attribute, ANDROID_URI, value, true);
+ element.commitDirtyAttributesToXml();
+ }
+ });
+ break;
+ }
+ case LINK_DISABLE_SANDBOX: {
+ RenderSecurityManager.sEnabled = false;
+ recomputeLayout();
+
+ MessageDialog.openInformation(AdtPlugin.getShell(),
+ "Disabled Rendering Sandbox",
+ "The custom view rendering sandbox was disabled for this session.\n\n" +
+ "You can turn it off permanently by adding\n" +
+ "-D" + ENABLED_PROPERTY + "=" + VALUE_FALSE + "\n" +
+ "as a new line in eclipse.ini.");
+
+ break;
+ }
+ default:
+ assert false : mAction;
+ break;
+ }
+ }
+
+ @Override
+ public boolean similarTo(StyleRange style) {
+ // Prevent adjacent link ranges from getting merged
+ return false;
+ }
+ }
+
+ /**
+ * Returns the error label for the graphical editor (which may not be visible
+ * or showing errors)
+ *
+ * @return the error label, never null
+ */
+ StyledText getErrorLabel() {
+ return mErrorLabel;
+ }
+
+ /**
+ * Monitor clicks on the error label.
+ * If the click happens on a style range created by
+ * {@link GraphicalEditorPart#addClassLink(StyledText, String)}, we assume it's about
+ * a missing class and we then proceed to display the standard Eclipse class creator wizard.
+ */
+ private class ErrorLabelListener extends MouseAdapter {
+
+ @Override
+ public void mouseUp(MouseEvent event) {
+ super.mouseUp(event);
+
+ if (event.widget != mErrorLabel) {
+ return;
+ }
+
+ int offset = mErrorLabel.getCaretOffset();
+
+ StyleRange r = null;
+ StyleRange[] ranges = mErrorLabel.getStyleRanges();
+ if (ranges != null && ranges.length > 0) {
+ for (StyleRange sr : ranges) {
+ if (sr.start <= offset && sr.start + sr.length > offset) {
+ r = sr;
+ break;
+ }
+ }
+ }
+
+ if (r instanceof ActionLinkStyleRange) {
+ ActionLinkStyleRange range = (ActionLinkStyleRange) r;
+ range.onClick();
+ }
+
+ LayoutCanvas canvas = getCanvasControl();
+ canvas.updateMenuActionState();
+ }
+ }
+
+ private void createNewClass(String fqcn) {
+
+ int pos = fqcn.lastIndexOf('.');
+ String packageName = pos < 0 ? "" : fqcn.substring(0, pos); //$NON-NLS-1$
+ String className = pos <= 0 || pos >= fqcn.length() ? "" : fqcn.substring(pos + 1); //$NON-NLS-1$
+
+ // create the wizard page for the class creation, and configure it
+ NewClassWizardPage page = new NewClassWizardPage();
+
+ // set the parent class
+ page.setSuperClass(SdkConstants.CLASS_VIEW, true /* canBeModified */);
+
+ // get the source folders as java elements.
+ IPackageFragmentRoot[] roots = getPackageFragmentRoots(
+ mEditorDelegate.getEditor().getProject(),
+ false /*includeContainers*/, true /*skipGenFolder*/);
+
+ IPackageFragmentRoot currentRoot = null;
+ IPackageFragment currentFragment = null;
+ int packageMatchCount = -1;
+
+ for (IPackageFragmentRoot root : roots) {
+ // Get the java element for the package.
+ // This method is said to always return a IPackageFragment even if the
+ // underlying folder doesn't exist...
+ IPackageFragment fragment = root.getPackageFragment(packageName);
+ if (fragment != null && fragment.exists()) {
+ // we have a perfect match! we use it.
+ currentRoot = root;
+ currentFragment = fragment;
+ packageMatchCount = -1;
+ break;
+ } else {
+ // we don't have a match. we look for the fragment with the best match
+ // (ie the closest parent package we can find)
+ try {
+ IJavaElement[] children;
+ children = root.getChildren();
+ for (IJavaElement child : children) {
+ if (child instanceof IPackageFragment) {
+ fragment = (IPackageFragment)child;
+ if (packageName.startsWith(fragment.getElementName())) {
+ // its a match. get the number of segments
+ String[] segments = fragment.getElementName().split("\\."); //$NON-NLS-1$
+ if (segments.length > packageMatchCount) {
+ packageMatchCount = segments.length;
+ currentFragment = fragment;
+ currentRoot = root;
+ }
+ }
+ }
+ }
+ } catch (JavaModelException e) {
+ // Couldn't get the children: we just ignore this package root.
+ }
+ }
+ }
+
+ ArrayList<IPackageFragment> createdFragments = null;
+
+ if (currentRoot != null) {
+ // if we have a perfect match, we set it and we're done.
+ if (packageMatchCount == -1) {
+ page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
+ page.setPackageFragment(currentFragment, true /* canBeModified */);
+ } else {
+ // we have a partial match.
+ // create the package. We have to start with the first segment so that we
+ // know what to delete in case of a cancel.
+ try {
+ createdFragments = new ArrayList<IPackageFragment>();
+
+ int totalCount = packageName.split("\\.").length; //$NON-NLS-1$
+ int count = 0;
+ int index = -1;
+ // skip the matching packages
+ while (count < packageMatchCount) {
+ index = packageName.indexOf('.', index+1);
+ count++;
+ }
+
+ // create the rest of the segments, except for the last one as indexOf will
+ // return -1;
+ while (count < totalCount - 1) {
+ index = packageName.indexOf('.', index+1);
+ count++;
+ createdFragments.add(currentRoot.createPackageFragment(
+ packageName.substring(0, index),
+ true /* force*/, new NullProgressMonitor()));
+ }
+
+ // create the last package
+ createdFragments.add(currentRoot.createPackageFragment(
+ packageName, true /* force*/, new NullProgressMonitor()));
+
+ // set the root and fragment in the Wizard page
+ page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
+ page.setPackageFragment(createdFragments.get(createdFragments.size()-1),
+ true /* canBeModified */);
+ } catch (JavaModelException e) {
+ // If we can't create the packages, there's a problem.
+ // We revert to the default package
+ for (IPackageFragmentRoot root : roots) {
+ // Get the java element for the package.
+ // This method is said to always return a IPackageFragment even if the
+ // underlying folder doesn't exist...
+ IPackageFragment fragment = root.getPackageFragment(packageName);
+ if (fragment != null && fragment.exists()) {
+ page.setPackageFragmentRoot(root, true /* canBeModified*/);
+ page.setPackageFragment(fragment, true /* canBeModified */);
+ break;
+ }
+ }
+ }
+ }
+ } else if (roots.length > 0) {
+ // if we haven't found a valid fragment, we set the root to the first source folder.
+ page.setPackageFragmentRoot(roots[0], true /* canBeModified*/);
+ }
+
+ // if we have a starting class name we use it
+ if (className != null) {
+ page.setTypeName(className, true /* canBeModified*/);
+ }
+
+ // create the action that will open it the wizard.
+ OpenNewClassWizardAction action = new OpenNewClassWizardAction();
+ action.setConfiguredWizardPage(page);
+ action.run();
+ IJavaElement element = action.getCreatedElement();
+
+ if (element == null) {
+ // lets delete the packages we created just for this.
+ // we need to start with the leaf and go up
+ if (createdFragments != null) {
+ try {
+ for (int i = createdFragments.size() - 1 ; i >= 0 ; i--) {
+ createdFragments.get(i).delete(true /* force*/,
+ new NullProgressMonitor());
+ }
+ } catch (JavaModelException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /**
+ * Computes and return the {@link IPackageFragmentRoot}s corresponding to the source
+ * folders of the specified project.
+ *
+ * @param project the project
+ * @param includeContainers True to include containers
+ * @param skipGenFolder True to skip the "gen" folder
+ * @return an array of IPackageFragmentRoot.
+ */
+ private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project,
+ boolean includeContainers, boolean skipGenFolder) {
+ ArrayList<IPackageFragmentRoot> result = new ArrayList<IPackageFragmentRoot>();
+ try {
+ IJavaProject javaProject = JavaCore.create(project);
+ IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots();
+ for (int i = 0; i < roots.length; i++) {
+ if (skipGenFolder) {
+ IResource resource = roots[i].getResource();
+ if (resource != null && resource.getName().equals(FD_GEN_SOURCES)) {
+ continue;
+ }
+ }
+ IClasspathEntry entry = roots[i].getRawClasspathEntry();
+ if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE ||
+ (includeContainers &&
+ entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER)) {
+ result.add(roots[i]);
+ }
+ }
+ } catch (JavaModelException e) {
+ }
+
+ return result.toArray(new IPackageFragmentRoot[result.size()]);
+ }
+
+ /**
+ * Reopens this file as included within the given file (this assumes that the given
+ * file has an include tag referencing this view, and the set of views that have this
+ * property can be found using the {@link IncludeFinder}.
+ *
+ * @param includeWithin reference to a file to include as a surrounding context,
+ * or null to show the file standalone
+ */
+ public void showIn(Reference includeWithin) {
+ mIncludedWithin = includeWithin;
+
+ if (includeWithin != null) {
+ IFile file = includeWithin.getFile();
+
+ // Update configuration
+ if (file != null) {
+ mConfigChooser.resetConfigFor(file);
+ }
+ }
+ recomputeLayout();
+ }
+
+ /**
+ * Return all resource names of a given type, either in the project or in the
+ * framework.
+ *
+ * @param framework if true, return all the framework resource names, otherwise return
+ * all the project resource names
+ * @param type the type of resource to look up
+ * @return a collection of resource names, never null but possibly empty
+ */
+ public Collection<String> getResourceNames(boolean framework, ResourceType type) {
+ Map<ResourceType, Map<String, ResourceValue>> map =
+ framework ? mConfiguredFrameworkRes : mConfiguredProjectRes;
+ Map<String, ResourceValue> animations = map.get(type);
+ if (animations != null) {
+ return animations.keySet();
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Return this editor's current configuration
+ *
+ * @return the current configuration
+ */
+ public FolderConfiguration getConfiguration() {
+ return mConfigChooser.getConfiguration().getFullConfig();
+ }
+
+ /**
+ * Figures out the project's minSdkVersion and targetSdkVersion and return whether the values
+ * have changed.
+ */
+ private boolean computeSdkVersion() {
+ int oldMinSdkVersion = mMinSdkVersion;
+ int oldTargetSdkVersion = mTargetSdkVersion;
+
+ Pair<Integer, Integer> v = ManifestInfo.computeSdkVersions(mEditedFile.getProject());
+ mMinSdkVersion = v.getFirst();
+ mTargetSdkVersion = v.getSecond();
+
+ return oldMinSdkVersion != mMinSdkVersion || oldTargetSdkVersion != mTargetSdkVersion;
+ }
+
+ /**
+ * Returns the associated configuration chooser
+ *
+ * @return the configuration chooser
+ */
+ @NonNull
+ public ConfigurationChooser getConfigurationChooser() {
+ return mConfigChooser;
+ }
+
+ /**
+ * Returns the associated layout actions bar
+ *
+ * @return the layout actions bar
+ */
+ @NonNull
+ public LayoutActionBar getLayoutActionBar() {
+ return mActionBar;
+ }
+
+ /**
+ * Returns the target SDK version
+ *
+ * @return the target SDK version
+ */
+ public int getTargetSdkVersion() {
+ return mTargetSdkVersion;
+ }
+
+ /**
+ * Returns the minimum SDK version
+ *
+ * @return the minimum SDK version
+ */
+ public int getMinSdkVersion() {
+ return mMinSdkVersion;
+ }
+
+ /** If the flyout hover is showing, dismiss it */
+ public void dismissHoverPalette() {
+ mPaletteComposite.dismissHover();
+ }
+
+ // ---- Implements IFlyoutListener ----
+
+ @Override
+ public void stateChanged(int oldState, int newState) {
+ // Auto zoom the surface if you open or close flyout windows such as the palette
+ // or the property/outline views
+ if (newState == STATE_OPEN || newState == STATE_COLLAPSED && oldState == STATE_OPEN) {
+ getCanvasControl().setFitScale(true /*onlyZoomOut*/, true /*allowZoomIn*/);
+ }
+
+ sDockingStateVersion++;
+ mDockingStateVersion = sDockingStateVersion;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/HoverOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/HoverOverlay.java
new file mode 100644
index 000000000..2e7c559db
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/HoverOverlay.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2010 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 static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtDrawingStyle.HOVER;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtDrawingStyle.HOVER_SELECTION;
+
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.util.List;
+
+/**
+ * The {@link HoverOverlay} paints an optional hover on top of the layout,
+ * highlighting the currently hovered view.
+ */
+public class HoverOverlay extends Overlay {
+ private final LayoutCanvas mCanvas;
+
+ /** Hover border color. Must be disposed, it's NOT a system color. */
+ private Color mHoverStrokeColor;
+
+ /** Hover fill color. Must be disposed, it's NOT a system color. */
+ private Color mHoverFillColor;
+
+ /** Hover border select color. Must be disposed, it's NOT a system color. */
+ private Color mHoverSelectStrokeColor;
+
+ /** Hover fill select color. Must be disposed, it's NOT a system color. */
+ private Color mHoverSelectFillColor;
+
+ /** Vertical scaling & scrollbar information. */
+ private CanvasTransform mVScale;
+
+ /** Horizontal scaling & scrollbar information. */
+ private CanvasTransform mHScale;
+
+ /**
+ * Current mouse hover border rectangle. Null when there's no mouse hover.
+ * The rectangle coordinates do not take account of the translation, which
+ * must be applied to the rectangle when drawing.
+ */
+ private Rectangle mHoverRect;
+
+ /**
+ * Constructs a new {@link HoverOverlay} linked to the given view hierarchy.
+ *
+ * @param canvas the associated canvas
+ * @param hScale The {@link CanvasTransform} to use to transfer horizontal layout
+ * coordinates to screen coordinates.
+ * @param vScale The {@link CanvasTransform} to use to transfer vertical layout
+ * coordinates to screen coordinates.
+ */
+ public HoverOverlay(LayoutCanvas canvas, CanvasTransform hScale, CanvasTransform vScale) {
+ mCanvas = canvas;
+ mHScale = hScale;
+ mVScale = vScale;
+ }
+
+ @Override
+ public void create(Device device) {
+ if (SwtDrawingStyle.HOVER.getStrokeColor() != null) {
+ mHoverStrokeColor = new Color(device, SwtDrawingStyle.HOVER.getStrokeColor());
+ }
+ if (SwtDrawingStyle.HOVER.getFillColor() != null) {
+ mHoverFillColor = new Color(device, SwtDrawingStyle.HOVER.getFillColor());
+ }
+
+ if (SwtDrawingStyle.HOVER_SELECTION.getStrokeColor() != null) {
+ mHoverSelectStrokeColor = new Color(device,
+ SwtDrawingStyle.HOVER_SELECTION.getStrokeColor());
+ }
+ if (SwtDrawingStyle.HOVER_SELECTION.getFillColor() != null) {
+ mHoverSelectFillColor = new Color(device,
+ SwtDrawingStyle.HOVER_SELECTION.getFillColor());
+ }
+ }
+
+ @Override
+ public void dispose() {
+ if (mHoverStrokeColor != null) {
+ mHoverStrokeColor.dispose();
+ mHoverStrokeColor = null;
+ }
+
+ if (mHoverFillColor != null) {
+ mHoverFillColor.dispose();
+ mHoverFillColor = null;
+ }
+
+ if (mHoverSelectStrokeColor != null) {
+ mHoverSelectStrokeColor.dispose();
+ mHoverSelectStrokeColor = null;
+ }
+
+ if (mHoverSelectFillColor != null) {
+ mHoverSelectFillColor.dispose();
+ mHoverSelectFillColor = null;
+ }
+ }
+
+ /**
+ * Sets the hover rectangle. The coordinates of the rectangle are in layout
+ * coordinates. The recipient is will own this rectangle.
+ * <p/>
+ * TODO: Consider switching input arguments to two {@link LayoutPoint}s so
+ * we don't have ambiguity about the coordinate system of these input
+ * parameters.
+ * <p/>
+ *
+ * @param x The top left x coordinate, in layout coordinates, of the hover.
+ * @param y The top left y coordinate, in layout coordinates, of the hover.
+ * @param w The width of the hover (in layout coordinates).
+ * @param h The height of the hover (in layout coordinates).
+ */
+ public void setHover(int x, int y, int w, int h) {
+ mHoverRect = new Rectangle(x, y, w, h);
+ }
+
+ /**
+ * Removes the hover for the next paint.
+ */
+ public void clearHover() {
+ mHoverRect = null;
+ }
+
+ @Override
+ public void paint(GC gc) {
+ if (mHoverRect != null) {
+ // Translate the hover rectangle (in canvas coordinates) to control
+ // coordinates
+ int x = mHScale.translate(mHoverRect.x);
+ int y = mVScale.translate(mHoverRect.y);
+ int w = mHScale.scale(mHoverRect.width);
+ int h = mVScale.scale(mHoverRect.height);
+
+
+ boolean hoverIsSelected = false;
+ List<SelectionItem> selections = mCanvas.getSelectionManager().getSelections();
+ for (SelectionItem item : selections) {
+ if (mHoverRect.equals(item.getViewInfo().getSelectionRect())) {
+ hoverIsSelected = true;
+ break;
+ }
+ }
+
+ Color stroke = hoverIsSelected ? mHoverSelectStrokeColor : mHoverStrokeColor;
+ Color fill = hoverIsSelected ? mHoverSelectFillColor : mHoverFillColor;
+
+ if (stroke != null) {
+ int oldAlpha = gc.getAlpha();
+ gc.setForeground(stroke);
+ gc.setLineStyle(hoverIsSelected ?
+ HOVER_SELECTION.getLineStyle() : HOVER.getLineStyle());
+ gc.setAlpha(hoverIsSelected ?
+ HOVER_SELECTION.getStrokeAlpha() : HOVER.getStrokeAlpha());
+ gc.drawRectangle(x, y, w, h);
+ gc.setAlpha(oldAlpha);
+ }
+
+ if (fill != null) {
+ int oldAlpha = gc.getAlpha();
+ gc.setAlpha(hoverIsSelected ?
+ HOVER_SELECTION.getFillAlpha() : HOVER.getFillAlpha());
+ gc.setBackground(fill);
+ gc.fillRectangle(x, y, w, h);
+ gc.setAlpha(oldAlpha);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageControl.java
new file mode 100644
index 000000000..4447eebd2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageControl.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2011 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.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * An ImageControl which simply renders an image, with optional margins and tooltips. This
+ * is useful since a {@link CLabel}, even without text, will hide the image when there is
+ * not enough room to fully fit it.
+ * <p>
+ * The image is always rendered left and top aligned.
+ */
+public class ImageControl extends Canvas implements MouseTrackListener {
+ private Image mImage;
+ private int mLeftMargin;
+ private int mTopMargin;
+ private int mRightMargin;
+ private int mBottomMargin;
+ private boolean mDisposeImage = true;
+ private boolean mMouseIn;
+ private Color mHoverColor;
+ private float mScale = 1.0f;
+
+ /**
+ * Creates an ImageControl rendering the given image, which will be disposed when this
+ * control is disposed (unless the {@link #setDisposeImage} method is called to turn
+ * off auto dispose).
+ *
+ * @param parent the parent to add the image control to
+ * @param style the SWT style to use
+ * @param image the image to be rendered, which must not be null and should be unique
+ * for this image control since it will be disposed by this control when
+ * the control is disposed (unless the {@link #setDisposeImage} method is
+ * called to turn off auto dispose)
+ */
+ public ImageControl(@NonNull Composite parent, int style, @Nullable Image image) {
+ super(parent, style | SWT.NO_FOCUS | SWT.DOUBLE_BUFFERED);
+ mImage = image;
+
+ addPaintListener(new PaintListener() {
+ @Override
+ public void paintControl(PaintEvent event) {
+ onPaint(event);
+ }
+ });
+ }
+
+ @Nullable
+ public Image getImage() {
+ return mImage;
+ }
+
+ public void setImage(@Nullable Image image) {
+ if (mDisposeImage && mImage != null) {
+ mImage.dispose();
+ }
+ mImage = image;
+ redraw();
+ }
+
+ public void fitToWidth(int width) {
+ if (mImage == null) {
+ return;
+ }
+ Rectangle imageRect = mImage.getBounds();
+ int imageWidth = imageRect.width;
+ if (imageWidth <= width) {
+ mScale = 1.0f;
+ return;
+ }
+
+ mScale = width / (float) imageWidth;
+ redraw();
+ }
+
+ public void setScale(float scale) {
+ mScale = scale;
+ }
+
+ public float getScale() {
+ return mScale;
+ }
+
+ public void setHoverColor(@Nullable Color hoverColor) {
+ if (mHoverColor != null) {
+ removeMouseTrackListener(this);
+ }
+ mHoverColor = hoverColor;
+ if (hoverColor != null) {
+ addMouseTrackListener(this);
+ }
+ }
+
+ @Nullable
+ public Color getHoverColor() {
+ return mHoverColor;
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+
+ if (mDisposeImage && mImage != null && !mImage.isDisposed()) {
+ mImage.dispose();
+ }
+ mImage = null;
+ }
+
+ public void setDisposeImage(boolean disposeImage) {
+ mDisposeImage = disposeImage;
+ }
+
+ public boolean getDisposeImage() {
+ return mDisposeImage;
+ }
+
+ @Override
+ public Point computeSize(int wHint, int hHint, boolean changed) {
+ checkWidget();
+ Point e = new Point(0, 0);
+ if (mImage != null) {
+ Rectangle r = mImage.getBounds();
+ if (mScale != 1.0f) {
+ e.x += mScale * r.width;
+ e.y += mScale * r.height;
+ } else {
+ e.x += r.width;
+ e.y += r.height;
+ }
+ }
+ if (wHint == SWT.DEFAULT) {
+ e.x += mLeftMargin + mRightMargin;
+ } else {
+ e.x = wHint;
+ }
+ if (hHint == SWT.DEFAULT) {
+ e.y += mTopMargin + mBottomMargin;
+ } else {
+ e.y = hHint;
+ }
+
+ return e;
+ }
+
+ private void onPaint(PaintEvent event) {
+ Rectangle rect = getClientArea();
+ if (mImage == null || rect.width == 0 || rect.height == 0) {
+ return;
+ }
+
+ GC gc = event.gc;
+ Rectangle imageRect = mImage.getBounds();
+ int imageHeight = imageRect.height;
+ int imageWidth = imageRect.width;
+ int destWidth = imageWidth;
+ int destHeight = imageHeight;
+
+ int oldGcAlias = gc.getAntialias();
+ int oldGcInterpolation = gc.getInterpolation();
+ if (mScale != 1.0f) {
+ destWidth = (int) (mScale * destWidth);
+ destHeight = (int) (mScale * destHeight);
+ gc.setAntialias(SWT.ON);
+ gc.setInterpolation(SWT.HIGH);
+ }
+
+ gc.drawImage(mImage, 0, 0, imageWidth, imageHeight, rect.x + mLeftMargin, rect.y
+ + mTopMargin, destWidth, destHeight);
+
+ gc.setAntialias(oldGcAlias);
+ gc.setInterpolation(oldGcInterpolation);
+
+ if (mHoverColor != null && mMouseIn) {
+ gc.setAlpha(60);
+ gc.setBackground(mHoverColor);
+ gc.setLineWidth(1);
+ gc.fillRectangle(0, 0, destWidth, destHeight);
+ }
+ }
+
+ public void setMargins(int leftMargin, int topMargin, int rightMargin, int bottomMargin) {
+ checkWidget();
+ mLeftMargin = Math.max(0, leftMargin);
+ mTopMargin = Math.max(0, topMargin);
+ mRightMargin = Math.max(0, rightMargin);
+ mBottomMargin = Math.max(0, bottomMargin);
+ redraw();
+ }
+
+ // ---- Implements MouseTrackListener ----
+
+ @Override
+ public void mouseEnter(MouseEvent e) {
+ mMouseIn = true;
+ if (mHoverColor != null) {
+ redraw();
+ }
+ }
+
+ @Override
+ public void mouseExit(MouseEvent e) {
+ mMouseIn = false;
+ if (mHoverColor != null) {
+ redraw();
+ }
+ }
+
+ @Override
+ public void mouseHover(MouseEvent e) {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java
new file mode 100644
index 000000000..a1363ecb1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright (C) 2010 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 static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE;
+
+import com.android.SdkConstants;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.rendering.api.IImageFactory;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.WritableRaster;
+import java.lang.ref.SoftReference;
+
+/**
+ * The {@link ImageOverlay} class renders an image as an overlay.
+ */
+public class ImageOverlay extends Overlay implements IImageFactory {
+ /**
+ * Whether the image should be pre-scaled (scaled to the zoom level) once
+ * instead of dynamically during each paint; this is necessary on some
+ * platforms (see issue #19447)
+ */
+ private static final boolean PRESCALE =
+ // Currently this is necessary on Linux because the "Cairo" library
+ // seems to be a bottleneck
+ SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX
+ && !(Boolean.getBoolean("adt.noprescale")); //$NON-NLS-1$
+
+ /** Current background image. Null when there's no image. */
+ private Image mImage;
+
+ /** A pre-scaled version of the image */
+ private Image mPreScaledImage;
+
+ /** Whether the rendered image should have a drop shadow */
+ private boolean mShowDropShadow;
+
+ /** Current background AWT image. This is created by {@link #getImage()}, which is called
+ * by the LayoutLib. */
+ private SoftReference<BufferedImage> mAwtImage = new SoftReference<BufferedImage>(null);
+
+ /**
+ * Strong reference to the image in the above soft reference, to prevent
+ * garbage collection when {@link PRESCALE} is set, until the scaled image
+ * is created (lazily as part of the next paint call, where this strong
+ * reference is nulled out and the above soft reference becomes eligible to
+ * be reclaimed when memory is low.)
+ */
+ @SuppressWarnings("unused") // Used by the garbage collector to keep mAwtImage non-soft
+ private BufferedImage mAwtImageStrongRef;
+
+ /** The associated {@link LayoutCanvas}. */
+ private LayoutCanvas mCanvas;
+
+ /** Vertical scaling & scrollbar information. */
+ private CanvasTransform mVScale;
+
+ /** Horizontal scaling & scrollbar information. */
+ private CanvasTransform mHScale;
+
+ /**
+ * Constructs an {@link ImageOverlay} tied to the given canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} to paint the overlay over.
+ * @param hScale The horizontal scale information.
+ * @param vScale The vertical scale information.
+ */
+ public ImageOverlay(LayoutCanvas canvas, CanvasTransform hScale, CanvasTransform vScale) {
+ mCanvas = canvas;
+ mHScale = hScale;
+ mVScale = vScale;
+ }
+
+ @Override
+ public void create(Device device) {
+ super.create(device);
+ }
+
+ @Override
+ public void dispose() {
+ if (mImage != null) {
+ mImage.dispose();
+ mImage = null;
+ }
+ if (mPreScaledImage != null) {
+ mPreScaledImage.dispose();
+ mPreScaledImage = null;
+ }
+ }
+
+ /**
+ * Sets the image to be drawn as an overlay from the passed in AWT
+ * {@link BufferedImage} (which will be converted to an SWT image).
+ * <p/>
+ * The image <b>can</b> be null, which is the case when we are dealing with
+ * an empty document.
+ *
+ * @param awtImage The AWT image to be rendered as an SWT image.
+ * @param isAlphaChannelImage whether the alpha channel of the image is relevant
+ * @return The corresponding SWT image, or null.
+ */
+ public synchronized Image setImage(BufferedImage awtImage, boolean isAlphaChannelImage) {
+ mShowDropShadow = !isAlphaChannelImage;
+
+ BufferedImage oldAwtImage = mAwtImage.get();
+ if (awtImage != oldAwtImage || awtImage == null) {
+ mAwtImage.clear();
+ mAwtImageStrongRef = null;
+
+ if (mImage != null) {
+ mImage.dispose();
+ }
+
+ if (awtImage == null) {
+ mImage = null;
+ } else {
+ mImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), awtImage,
+ isAlphaChannelImage, -1);
+ }
+ } else {
+ assert awtImage instanceof SwtReadyBufferedImage;
+
+ if (isAlphaChannelImage) {
+ if (mImage != null) {
+ mImage.dispose();
+ }
+
+ mImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), awtImage, true, -1);
+ } else {
+ Image prev = mImage;
+ mImage = ((SwtReadyBufferedImage)awtImage).getSwtImage();
+ if (prev != mImage && prev != null) {
+ prev.dispose();
+ }
+ }
+ }
+
+ if (mPreScaledImage != null) {
+ // Force refresh on next paint
+ mPreScaledImage.dispose();
+ mPreScaledImage = null;
+ }
+
+ return mImage;
+ }
+
+ /**
+ * Returns the currently painted image, or null if none has been set
+ *
+ * @return the currently painted image or null
+ */
+ public Image getImage() {
+ return mImage;
+ }
+
+ /**
+ * Returns the currently rendered image, or null if none has been set
+ *
+ * @return the currently rendered image or null
+ */
+ @Nullable
+ BufferedImage getAwtImage() {
+ BufferedImage awtImage = mAwtImage.get();
+ if (awtImage == null && mImage != null) {
+ awtImage = SwtUtils.convertToAwt(mImage);
+ }
+
+ return awtImage;
+ }
+
+ /**
+ * Returns whether this image overlay should be painted with a drop shadow.
+ * This is usually the case, but not for transparent themes like the dialog
+ * theme (Theme.*Dialog), which already provides its own shadow.
+ *
+ * @return true if the image overlay should be shown with a drop shadow.
+ */
+ public boolean getShowDropShadow() {
+ return mShowDropShadow;
+ }
+
+ @Override
+ public synchronized void paint(GC gc) {
+ if (mImage != null) {
+ boolean valid = mCanvas.getViewHierarchy().isValid();
+ mCanvas.ensureZoomed();
+ if (!valid) {
+ gc_setAlpha(gc, 128); // half-transparent
+ }
+
+ CanvasTransform hi = mHScale;
+ CanvasTransform vi = mVScale;
+
+ // On some platforms, dynamic image scaling is very slow (see issue #19447) so
+ // compute a pre-scaled version of the image once and render that instead.
+ // This is done lazily in paint rather than when the image changes because
+ // the image must be rescaled each time the zoom level changes, which varies
+ // independently from when the image changes.
+ BufferedImage awtImage = mAwtImage.get();
+ if (PRESCALE && awtImage != null) {
+ int imageWidth = (mPreScaledImage == null) ? 0
+ : mPreScaledImage.getImageData().width
+ - (mShowDropShadow ? SHADOW_SIZE : 0);
+ if (mPreScaledImage == null || imageWidth != hi.getScaledImgSize()) {
+ double xScale = hi.getScaledImgSize() / (double) awtImage.getWidth();
+ double yScale = vi.getScaledImgSize() / (double) awtImage.getHeight();
+ BufferedImage scaledAwtImage;
+
+ // NOTE: == comparison on floating point numbers is okay
+ // here because we normalize the scaling factor
+ // to an exact 1.0 in the zooming code when the value gets
+ // near 1.0 to make painting more efficient in the presence
+ // of rounding errors.
+ if (xScale == 1.0 && yScale == 1.0) {
+ // Scaling to 100% is easy!
+ scaledAwtImage = awtImage;
+
+ if (mShowDropShadow) {
+ // Just need to draw drop shadows
+ scaledAwtImage = ImageUtils.createRectangularDropShadow(awtImage);
+ }
+ } else {
+ if (mShowDropShadow) {
+ scaledAwtImage = ImageUtils.scale(awtImage, xScale, yScale,
+ SHADOW_SIZE, SHADOW_SIZE);
+ ImageUtils.drawRectangleShadow(scaledAwtImage, 0, 0,
+ scaledAwtImage.getWidth() - SHADOW_SIZE,
+ scaledAwtImage.getHeight() - SHADOW_SIZE);
+ } else {
+ scaledAwtImage = ImageUtils.scale(awtImage, xScale, yScale);
+ }
+ }
+
+ if (mPreScaledImage != null && !mPreScaledImage.isDisposed()) {
+ mPreScaledImage.dispose();
+ }
+ mPreScaledImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), scaledAwtImage,
+ true /*transferAlpha*/, -1);
+ // We can't just clear the mAwtImageStrongRef here, because if the
+ // zooming factor changes, we may need to use it again
+ }
+
+ if (mPreScaledImage != null) {
+ gc.drawImage(mPreScaledImage, hi.translate(0), vi.translate(0));
+ }
+ return;
+ }
+
+ // we only anti-alias when reducing the image size.
+ int oldAlias = -2;
+ if (hi.getScale() < 1.0) {
+ oldAlias = gc_setAntialias(gc, SWT.ON);
+ }
+
+ int srcX = 0;
+ int srcY = 0;
+ int srcWidth = hi.getImgSize();
+ int srcHeight = vi.getImgSize();
+ int destX = hi.translate(0);
+ int destY = vi.translate(0);
+ int destWidth = hi.getScaledImgSize();
+ int destHeight = vi.getScaledImgSize();
+
+ gc.drawImage(mImage,
+ srcX, srcY, srcWidth, srcHeight,
+ destX, destY, destWidth, destHeight);
+
+ if (mShowDropShadow) {
+ SwtUtils.drawRectangleShadow(gc, destX, destY, destWidth, destHeight);
+ }
+
+ if (oldAlias != -2) {
+ gc_setAntialias(gc, oldAlias);
+ }
+
+ if (!valid) {
+ gc_setAlpha(gc, 255); // opaque
+ }
+ }
+ }
+
+ /**
+ * Sets the alpha for the given GC.
+ * <p/>
+ * Alpha may not work on all platforms and may fail with an exception, which
+ * is hidden here (false is returned in that case).
+ *
+ * @param gc the GC to change
+ * @param alpha the new alpha, 0 for transparent, 255 for opaque.
+ * @return True if the operation worked, false if it failed with an
+ * exception.
+ * @see GC#setAlpha(int)
+ */
+ private boolean gc_setAlpha(GC gc, int alpha) {
+ try {
+ gc.setAlpha(alpha);
+ return true;
+ } catch (SWTException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Sets the non-text antialias flag for the given GC.
+ * <p/>
+ * Antialias may not work on all platforms and may fail with an exception,
+ * which is hidden here (-2 is returned in that case).
+ *
+ * @param gc the GC to change
+ * @param alias One of {@link SWT#DEFAULT}, {@link SWT#ON}, {@link SWT#OFF}.
+ * @return The previous aliasing mode if the operation worked, or -2 if it
+ * failed with an exception.
+ * @see GC#setAntialias(int)
+ */
+ private int gc_setAntialias(GC gc, int alias) {
+ try {
+ int old = gc.getAntialias();
+ gc.setAntialias(alias);
+ return old;
+ } catch (SWTException e) {
+ return -2;
+ }
+ }
+
+ /**
+ * Custom {@link BufferedImage} class able to convert itself into an SWT {@link Image}
+ * efficiently.
+ *
+ * The BufferedImage also contains an instance of {@link ImageData} that's kept around
+ * and used to create new SWT {@link Image} objects in {@link #getSwtImage()}.
+ *
+ */
+ private static final class SwtReadyBufferedImage extends BufferedImage {
+
+ private final ImageData mImageData;
+ private final Device mDevice;
+
+ /**
+ * Creates the image with a given model, raster and SWT {@link ImageData}
+ * @param model the color model
+ * @param raster the image raster
+ * @param imageData the SWT image data.
+ * @param device the {@link Device} in which the SWT image will be painted.
+ */
+ private SwtReadyBufferedImage(int width, int height, ImageData imageData, Device device) {
+ super(width, height, BufferedImage.TYPE_INT_ARGB);
+ mImageData = imageData;
+ mDevice = device;
+ }
+
+ /**
+ * Returns a new {@link Image} object initialized with the content of the BufferedImage.
+ * @return the image object.
+ */
+ private Image getSwtImage() {
+ // transfer the content of the bufferedImage into the image data.
+ WritableRaster raster = getRaster();
+ int[] imageDataBuffer = ((DataBufferInt) raster.getDataBuffer()).getData();
+
+ mImageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
+
+ return new Image(mDevice, mImageData);
+ }
+
+ /**
+ * Creates a new {@link SwtReadyBufferedImage}.
+ * @param w the width of the image
+ * @param h the height of the image
+ * @param device the device in which the SWT image will be painted
+ * @return a new {@link SwtReadyBufferedImage} object
+ */
+ private static SwtReadyBufferedImage createImage(int w, int h, Device device) {
+ // NOTE: We can't make this image bigger to accommodate the drop shadow directly
+ // (such that we could paint one into the image after a layoutlib render)
+ // since this image is in the full resolution of the device, and gets scaled
+ // to fit in the layout editor. This would have the net effect of causing
+ // the drop shadow to get zoomed/scaled along with the scene, making a tiny
+ // drop shadow for tablet layouts, a huge drop shadow for tiny QVGA screens, etc.
+
+ ImageData imageData = new ImageData(w, h, 32,
+ new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF));
+
+ SwtReadyBufferedImage swtReadyImage = new SwtReadyBufferedImage(w, h,
+ imageData, device);
+
+ return swtReadyImage;
+ }
+ }
+
+ /**
+ * Implementation of {@link IImageFactory#getImage(int, int)}.
+ */
+ @Override
+ public BufferedImage getImage(int w, int h) {
+ BufferedImage awtImage = mAwtImage.get();
+ if (awtImage == null ||
+ awtImage.getWidth() != w ||
+ awtImage.getHeight() != h) {
+ mAwtImage.clear();
+ awtImage = SwtReadyBufferedImage.createImage(w, h, getDevice());
+ mAwtImage = new SoftReference<BufferedImage>(awtImage);
+ if (PRESCALE) {
+ mAwtImageStrongRef = awtImage;
+ }
+ }
+
+ return awtImage;
+ }
+
+ /**
+ * Returns the bounds of the current image, or null
+ *
+ * @return the bounds of the current image, or null
+ */
+ public Rect getImageBounds() {
+ if (mImage == null) {
+ return null;
+ }
+
+ return new Rect(0, 0, mImage.getImageData().width, mImage.getImageData().height);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java
new file mode 100644
index 000000000..b5bc9aa72
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java
@@ -0,0 +1,979 @@
+/*
+ * Copyright (C) 2010 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 static com.android.SdkConstants.DOT_9PNG;
+import static com.android.SdkConstants.DOT_BMP;
+import static com.android.SdkConstants.DOT_GIF;
+import static com.android.SdkConstants.DOT_JPG;
+import static com.android.SdkConstants.DOT_PNG;
+import static com.android.utils.SdkUtils.endsWithIgnoreCase;
+import static java.awt.RenderingHints.KEY_ANTIALIASING;
+import static java.awt.RenderingHints.KEY_INTERPOLATION;
+import static java.awt.RenderingHints.KEY_RENDERING;
+import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON;
+import static java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR;
+import static java.awt.RenderingHints.VALUE_RENDER_QUALITY;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.Rect;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.imageio.ImageIO;
+
+/**
+ * Utilities related to image processing.
+ */
+public class ImageUtils {
+ /**
+ * Returns true if the given image has no dark pixels
+ *
+ * @param image the image to be checked for dark pixels
+ * @return true if no dark pixels were found
+ */
+ public static boolean containsDarkPixels(BufferedImage image) {
+ for (int y = 0, height = image.getHeight(); y < height; y++) {
+ for (int x = 0, width = image.getWidth(); x < width; x++) {
+ int pixel = image.getRGB(x, y);
+ if ((pixel & 0xFF000000) != 0) {
+ int r = (pixel & 0xFF0000) >> 16;
+ int g = (pixel & 0x00FF00) >> 8;
+ int b = (pixel & 0x0000FF);
+
+ // One perceived luminance formula is (0.299*red + 0.587*green + 0.114*blue)
+ // In order to keep this fast since we don't need a very accurate
+ // measure, I'll just estimate this with integer math:
+ long brightness = (299L*r + 587*g + 114*b) / 1000;
+ if (brightness < 128) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the perceived brightness of the given RGB integer on a scale from 0 to 255
+ *
+ * @param rgb the RGB triplet, 8 bits each
+ * @return the perceived brightness, with 0 maximally dark and 255 maximally bright
+ */
+ public static int getBrightness(int rgb) {
+ if ((rgb & 0xFFFFFF) != 0) {
+ int r = (rgb & 0xFF0000) >> 16;
+ int g = (rgb & 0x00FF00) >> 8;
+ int b = (rgb & 0x0000FF);
+ // See the containsDarkPixels implementation for details
+ return (int) ((299L*r + 587*g + 114*b) / 1000);
+ }
+
+ return 0;
+ }
+
+ /**
+ * Converts an alpha-red-green-blue integer color into an {@link RGB} color.
+ * <p>
+ * <b>NOTE</b> - this will drop the alpha value since {@link RGB} objects do not
+ * contain transparency information.
+ *
+ * @param rgb the RGB integer to convert to a color description
+ * @return the color description corresponding to the integer
+ */
+ public static RGB intToRgb(int rgb) {
+ return new RGB((rgb & 0xFF0000) >>> 16, (rgb & 0xFF00) >>> 8, rgb & 0xFF);
+ }
+
+ /**
+ * Converts an {@link RGB} color into a alpha-red-green-blue integer
+ *
+ * @param rgb the RGB color descriptor to convert
+ * @param alpha the amount of alpha to add into the color integer (since the
+ * {@link RGB} objects do not contain an alpha channel)
+ * @return an integer corresponding to the {@link RGB} color
+ */
+ public static int rgbToInt(RGB rgb, int alpha) {
+ return alpha << 24 | (rgb.red << 16) | (rgb.green << 8) | rgb.blue;
+ }
+
+ /**
+ * Crops blank pixels from the edges of the image and returns the cropped result. We
+ * crop off pixels that are blank (meaning they have an alpha value = 0). Note that
+ * this is not the same as pixels that aren't opaque (an alpha value other than 255).
+ *
+ * @param image the image to be cropped
+ * @param initialCrop If not null, specifies a rectangle which contains an initial
+ * crop to continue. This can be used to crop an image where you already
+ * know about margins in the image
+ * @return a cropped version of the source image, or null if the whole image was blank
+ * and cropping completely removed everything
+ */
+ @Nullable
+ public static BufferedImage cropBlank(
+ @NonNull BufferedImage image,
+ @Nullable Rect initialCrop) {
+ return cropBlank(image, initialCrop, image.getType());
+ }
+
+ /**
+ * Crops blank pixels from the edges of the image and returns the cropped result. We
+ * crop off pixels that are blank (meaning they have an alpha value = 0). Note that
+ * this is not the same as pixels that aren't opaque (an alpha value other than 255).
+ *
+ * @param image the image to be cropped
+ * @param initialCrop If not null, specifies a rectangle which contains an initial
+ * crop to continue. This can be used to crop an image where you already
+ * know about margins in the image
+ * @param imageType the type of {@link BufferedImage} to create
+ * @return a cropped version of the source image, or null if the whole image was blank
+ * and cropping completely removed everything
+ */
+ public static BufferedImage cropBlank(BufferedImage image, Rect initialCrop, int imageType) {
+ CropFilter filter = new CropFilter() {
+ @Override
+ public boolean crop(BufferedImage bufferedImage, int x, int y) {
+ int rgb = bufferedImage.getRGB(x, y);
+ return (rgb & 0xFF000000) == 0x00000000;
+ // TODO: Do a threshold of 80 instead of just 0? Might give better
+ // visual results -- e.g. check <= 0x80000000
+ }
+ };
+ return crop(image, filter, initialCrop, imageType);
+ }
+
+ /**
+ * Crops pixels of a given color from the edges of the image and returns the cropped
+ * result.
+ *
+ * @param image the image to be cropped
+ * @param blankArgb the color considered to be blank, as a 32 pixel integer with 8
+ * bits of alpha, red, green and blue
+ * @param initialCrop If not null, specifies a rectangle which contains an initial
+ * crop to continue. This can be used to crop an image where you already
+ * know about margins in the image
+ * @return a cropped version of the source image, or null if the whole image was blank
+ * and cropping completely removed everything
+ */
+ @Nullable
+ public static BufferedImage cropColor(
+ @NonNull BufferedImage image,
+ final int blankArgb,
+ @Nullable Rect initialCrop) {
+ return cropColor(image, blankArgb, initialCrop, image.getType());
+ }
+
+ /**
+ * Crops pixels of a given color from the edges of the image and returns the cropped
+ * result.
+ *
+ * @param image the image to be cropped
+ * @param blankArgb the color considered to be blank, as a 32 pixel integer with 8
+ * bits of alpha, red, green and blue
+ * @param initialCrop If not null, specifies a rectangle which contains an initial
+ * crop to continue. This can be used to crop an image where you already
+ * know about margins in the image
+ * @param imageType the type of {@link BufferedImage} to create
+ * @return a cropped version of the source image, or null if the whole image was blank
+ * and cropping completely removed everything
+ */
+ public static BufferedImage cropColor(BufferedImage image,
+ final int blankArgb, Rect initialCrop, int imageType) {
+ CropFilter filter = new CropFilter() {
+ @Override
+ public boolean crop(BufferedImage bufferedImage, int x, int y) {
+ return blankArgb == bufferedImage.getRGB(x, y);
+ }
+ };
+ return crop(image, filter, initialCrop, imageType);
+ }
+
+ /**
+ * Interface implemented by cropping functions that determine whether
+ * a pixel should be cropped or not.
+ */
+ private static interface CropFilter {
+ /**
+ * Returns true if the pixel is should be cropped.
+ *
+ * @param image the image containing the pixel in question
+ * @param x the x position of the pixel
+ * @param y the y position of the pixel
+ * @return true if the pixel should be cropped (for example, is blank)
+ */
+ boolean crop(BufferedImage image, int x, int y);
+ }
+
+ private static BufferedImage crop(BufferedImage image, CropFilter filter, Rect initialCrop,
+ int imageType) {
+ if (image == null) {
+ return null;
+ }
+
+ // First, determine the dimensions of the real image within the image
+ int x1, y1, x2, y2;
+ if (initialCrop != null) {
+ x1 = initialCrop.x;
+ y1 = initialCrop.y;
+ x2 = initialCrop.x + initialCrop.w;
+ y2 = initialCrop.y + initialCrop.h;
+ } else {
+ x1 = 0;
+ y1 = 0;
+ x2 = image.getWidth();
+ y2 = image.getHeight();
+ }
+
+ // Nothing left to crop
+ if (x1 == x2 || y1 == y2) {
+ return null;
+ }
+
+ // This algorithm is a bit dumb -- it just scans along the edges looking for
+ // a pixel that shouldn't be cropped. I could maybe try to make it smarter by
+ // for example doing a binary search to quickly eliminate large empty areas to
+ // the right and bottom -- but this is slightly tricky with components like the
+ // AnalogClock where I could accidentally end up finding a blank horizontal or
+ // vertical line somewhere in the middle of the rendering of the clock, so for now
+ // we do the dumb thing -- not a big deal since we tend to crop reasonably
+ // small images.
+
+ // First determine top edge
+ topEdge: for (; y1 < y2; y1++) {
+ for (int x = x1; x < x2; x++) {
+ if (!filter.crop(image, x, y1)) {
+ break topEdge;
+ }
+ }
+ }
+
+ if (y1 == image.getHeight()) {
+ // The image is blank
+ return null;
+ }
+
+ // Next determine left edge
+ leftEdge: for (; x1 < x2; x1++) {
+ for (int y = y1; y < y2; y++) {
+ if (!filter.crop(image, x1, y)) {
+ break leftEdge;
+ }
+ }
+ }
+
+ // Next determine right edge
+ rightEdge: for (; x2 > x1; x2--) {
+ for (int y = y1; y < y2; y++) {
+ if (!filter.crop(image, x2 - 1, y)) {
+ break rightEdge;
+ }
+ }
+ }
+
+ // Finally determine bottom edge
+ bottomEdge: for (; y2 > y1; y2--) {
+ for (int x = x1; x < x2; x++) {
+ if (!filter.crop(image, x, y2 - 1)) {
+ break bottomEdge;
+ }
+ }
+ }
+
+ // No need to crop?
+ if (x1 == 0 && y1 == 0 && x2 == image.getWidth() && y2 == image.getHeight()) {
+ return image;
+ }
+
+ if (x1 == x2 || y1 == y2) {
+ // Nothing left after crop -- blank image
+ return null;
+ }
+
+ int width = x2 - x1;
+ int height = y2 - y1;
+
+ // Now extract the sub-image
+ if (imageType == -1) {
+ imageType = image.getType();
+ }
+ if (imageType == BufferedImage.TYPE_CUSTOM) {
+ imageType = BufferedImage.TYPE_INT_ARGB;
+ }
+ BufferedImage cropped = new BufferedImage(width, height, imageType);
+ Graphics g = cropped.getGraphics();
+ g.drawImage(image, 0, 0, width, height, x1, y1, x2, y2, null);
+
+ g.dispose();
+
+ return cropped;
+ }
+
+ /**
+ * Creates a drop shadow of a given image and returns a new image which shows the
+ * input image on top of its drop shadow.
+ * <p>
+ * <b>NOTE: If the shape is rectangular and opaque, consider using
+ * {@link #drawRectangleShadow(Graphics, int, int, int, int)} instead.</b>
+ *
+ * @param source the source image to be shadowed
+ * @param shadowSize the size of the shadow in pixels
+ * @param shadowOpacity the opacity of the shadow, with 0=transparent and 1=opaque
+ * @param shadowRgb the RGB int to use for the shadow color
+ * @return a new image with the source image on top of its shadow
+ */
+ public static BufferedImage createDropShadow(BufferedImage source, int shadowSize,
+ float shadowOpacity, int shadowRgb) {
+
+ // This code is based on
+ // http://www.jroller.com/gfx/entry/non_rectangular_shadow
+
+ BufferedImage image = new BufferedImage(source.getWidth() + shadowSize * 2,
+ source.getHeight() + shadowSize * 2,
+ BufferedImage.TYPE_INT_ARGB);
+
+ Graphics2D g2 = image.createGraphics();
+ g2.drawImage(source, null, shadowSize, shadowSize);
+
+ int dstWidth = image.getWidth();
+ int dstHeight = image.getHeight();
+
+ int left = (shadowSize - 1) >> 1;
+ int right = shadowSize - left;
+ int xStart = left;
+ int xStop = dstWidth - right;
+ int yStart = left;
+ int yStop = dstHeight - right;
+
+ shadowRgb = shadowRgb & 0x00FFFFFF;
+
+ int[] aHistory = new int[shadowSize];
+ int historyIdx = 0;
+
+ int aSum;
+
+ int[] dataBuffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
+ int lastPixelOffset = right * dstWidth;
+ float sumDivider = shadowOpacity / shadowSize;
+
+ // horizontal pass
+ for (int y = 0, bufferOffset = 0; y < dstHeight; y++, bufferOffset = y * dstWidth) {
+ aSum = 0;
+ historyIdx = 0;
+ for (int x = 0; x < shadowSize; x++, bufferOffset++) {
+ int a = dataBuffer[bufferOffset] >>> 24;
+ aHistory[x] = a;
+ aSum += a;
+ }
+
+ bufferOffset -= right;
+
+ for (int x = xStart; x < xStop; x++, bufferOffset++) {
+ int a = (int) (aSum * sumDivider);
+ dataBuffer[bufferOffset] = a << 24 | shadowRgb;
+
+ // subtract the oldest pixel from the sum
+ aSum -= aHistory[historyIdx];
+
+ // get the latest pixel
+ a = dataBuffer[bufferOffset + right] >>> 24;
+ aHistory[historyIdx] = a;
+ aSum += a;
+
+ if (++historyIdx >= shadowSize) {
+ historyIdx -= shadowSize;
+ }
+ }
+ }
+ // vertical pass
+ for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) {
+ aSum = 0;
+ historyIdx = 0;
+ for (int y = 0; y < shadowSize; y++, bufferOffset += dstWidth) {
+ int a = dataBuffer[bufferOffset] >>> 24;
+ aHistory[y] = a;
+ aSum += a;
+ }
+
+ bufferOffset -= lastPixelOffset;
+
+ for (int y = yStart; y < yStop; y++, bufferOffset += dstWidth) {
+ int a = (int) (aSum * sumDivider);
+ dataBuffer[bufferOffset] = a << 24 | shadowRgb;
+
+ // subtract the oldest pixel from the sum
+ aSum -= aHistory[historyIdx];
+
+ // get the latest pixel
+ a = dataBuffer[bufferOffset + lastPixelOffset] >>> 24;
+ aHistory[historyIdx] = a;
+ aSum += a;
+
+ if (++historyIdx >= shadowSize) {
+ historyIdx -= shadowSize;
+ }
+ }
+ }
+
+ g2.drawImage(source, null, 0, 0);
+ g2.dispose();
+
+ return image;
+ }
+
+ /**
+ * Draws a rectangular drop shadow (of size {@link #SHADOW_SIZE} by
+ * {@link #SHADOW_SIZE} around the given source and returns a new image with
+ * both combined
+ *
+ * @param source the source image
+ * @return the source image with a drop shadow on the bottom and right
+ */
+ public static BufferedImage createRectangularDropShadow(BufferedImage source) {
+ int type = source.getType();
+ if (type == BufferedImage.TYPE_CUSTOM) {
+ type = BufferedImage.TYPE_INT_ARGB;
+ }
+
+ int width = source.getWidth();
+ int height = source.getHeight();
+ BufferedImage image = new BufferedImage(width + SHADOW_SIZE, height + SHADOW_SIZE, type);
+ Graphics g = image.getGraphics();
+ g.drawImage(source, 0, 0, width, height, null);
+ ImageUtils.drawRectangleShadow(image, 0, 0, width, height);
+ g.dispose();
+
+ return image;
+ }
+
+ /**
+ * Draws a drop shadow for the given rectangle into the given context. It
+ * will not draw anything if the rectangle is smaller than a minimum
+ * determined by the assets used to draw the shadow graphics.
+ * The size of the shadow is {@link #SHADOW_SIZE}.
+ *
+ * @param image the image to draw the shadow into
+ * @param x the left coordinate of the left hand side of the rectangle
+ * @param y the top coordinate of the top of the rectangle
+ * @param width the width of the rectangle
+ * @param height the height of the rectangle
+ */
+ public static final void drawRectangleShadow(BufferedImage image,
+ int x, int y, int width, int height) {
+ Graphics gc = image.getGraphics();
+ try {
+ drawRectangleShadow(gc, x, y, width, height);
+ } finally {
+ gc.dispose();
+ }
+ }
+
+ /**
+ * Draws a small drop shadow for the given rectangle into the given context. It
+ * will not draw anything if the rectangle is smaller than a minimum
+ * determined by the assets used to draw the shadow graphics.
+ * The size of the shadow is {@link #SMALL_SHADOW_SIZE}.
+ *
+ * @param image the image to draw the shadow into
+ * @param x the left coordinate of the left hand side of the rectangle
+ * @param y the top coordinate of the top of the rectangle
+ * @param width the width of the rectangle
+ * @param height the height of the rectangle
+ */
+ public static final void drawSmallRectangleShadow(BufferedImage image,
+ int x, int y, int width, int height) {
+ Graphics gc = image.getGraphics();
+ try {
+ drawSmallRectangleShadow(gc, x, y, width, height);
+ } finally {
+ gc.dispose();
+ }
+ }
+
+ /**
+ * The width and height of the drop shadow painted by
+ * {@link #drawRectangleShadow(Graphics, int, int, int, int)}
+ */
+ public static final int SHADOW_SIZE = 20; // DO NOT EDIT. This corresponds to bitmap graphics
+
+ /**
+ * The width and height of the drop shadow painted by
+ * {@link #drawSmallRectangleShadow(Graphics, int, int, int, int)}
+ */
+ public static final int SMALL_SHADOW_SIZE = 10; // DO NOT EDIT. Corresponds to bitmap graphics
+
+ /**
+ * Draws a drop shadow for the given rectangle into the given context. It
+ * will not draw anything if the rectangle is smaller than a minimum
+ * determined by the assets used to draw the shadow graphics.
+ * <p>
+ * This corresponds to
+ * {@link SwtUtils#drawRectangleShadow(org.eclipse.swt.graphics.GC, int, int, int, int)},
+ * but applied to an AWT graphics object instead, such that no image
+ * conversion has to be performed.
+ * <p>
+ * Make sure to keep changes in the visual appearance here in sync with the
+ * AWT version in
+ * {@link SwtUtils#drawRectangleShadow(org.eclipse.swt.graphics.GC, int, int, int, int)}.
+ *
+ * @param gc the graphics context to draw into
+ * @param x the left coordinate of the left hand side of the rectangle
+ * @param y the top coordinate of the top of the rectangle
+ * @param width the width of the rectangle
+ * @param height the height of the rectangle
+ */
+ public static final void drawRectangleShadow(Graphics gc,
+ int x, int y, int width, int height) {
+ if (sShadowBottomLeft == null) {
+ // Shadow graphics. This was generated by creating a drop shadow in
+ // Gimp, using the parameters x offset=10, y offset=10, blur radius=10,
+ // color=black, and opacity=51. These values attempt to make a shadow
+ // that is legible both for dark and light themes, on top of the
+ // canvas background (rgb(150,150,150). Darker shadows would tend to
+ // blend into the foreground for a dark holo screen, and lighter shadows
+ // would be hard to spot on the canvas background. If you make adjustments,
+ // make sure to check the shadow with both dark and light themes.
+ //
+ // After making the graphics, I cut out the top right, bottom left
+ // and bottom right corners as 20x20 images, and these are reproduced by
+ // painting them in the corresponding places in the target graphics context.
+ // I then grabbed a single horizontal gradient line from the middle of the
+ // right edge,and a single vertical gradient line from the bottom. These
+ // are then painted scaled/stretched in the target to fill the gaps between
+ // the three corner images.
+ //
+ // Filenames: bl=bottom left, b=bottom, br=bottom right, r=right, tr=top right
+ sShadowBottomLeft = readImage("shadow-bl.png"); //$NON-NLS-1$
+ sShadowBottom = readImage("shadow-b.png"); //$NON-NLS-1$
+ sShadowBottomRight = readImage("shadow-br.png"); //$NON-NLS-1$
+ sShadowRight = readImage("shadow-r.png"); //$NON-NLS-1$
+ sShadowTopRight = readImage("shadow-tr.png"); //$NON-NLS-1$
+ assert sShadowBottomLeft != null;
+ assert sShadowBottomRight.getWidth() == SHADOW_SIZE;
+ assert sShadowBottomRight.getHeight() == SHADOW_SIZE;
+ }
+
+ int blWidth = sShadowBottomLeft.getWidth();
+ int trHeight = sShadowTopRight.getHeight();
+ if (width < blWidth) {
+ return;
+ }
+ if (height < trHeight) {
+ return;
+ }
+
+ gc.drawImage(sShadowBottomLeft, x, y + height, null);
+ gc.drawImage(sShadowBottomRight, x + width, y + height, null);
+ gc.drawImage(sShadowTopRight, x + width, y, null);
+ gc.drawImage(sShadowBottom,
+ x + sShadowBottomLeft.getWidth(), y + height,
+ x + width, y + height + sShadowBottom.getHeight(),
+ 0, 0, sShadowBottom.getWidth(), sShadowBottom.getHeight(),
+ null);
+ gc.drawImage(sShadowRight,
+ x + width, y + sShadowTopRight.getHeight(),
+ x + width + sShadowRight.getWidth(), y + height,
+ 0, 0, sShadowRight.getWidth(), sShadowRight.getHeight(),
+ null);
+ }
+
+ /**
+ * Draws a small drop shadow for the given rectangle into the given context. It
+ * will not draw anything if the rectangle is smaller than a minimum
+ * determined by the assets used to draw the shadow graphics.
+ * <p>
+ *
+ * @param gc the graphics context to draw into
+ * @param x the left coordinate of the left hand side of the rectangle
+ * @param y the top coordinate of the top of the rectangle
+ * @param width the width of the rectangle
+ * @param height the height of the rectangle
+ */
+ public static final void drawSmallRectangleShadow(Graphics gc,
+ int x, int y, int width, int height) {
+ if (sShadow2BottomLeft == null) {
+ // Shadow graphics. This was generated by creating a drop shadow in
+ // Gimp, using the parameters x offset=5, y offset=%, blur radius=5,
+ // color=black, and opacity=51. These values attempt to make a shadow
+ // that is legible both for dark and light themes, on top of the
+ // canvas background (rgb(150,150,150). Darker shadows would tend to
+ // blend into the foreground for a dark holo screen, and lighter shadows
+ // would be hard to spot on the canvas background. If you make adjustments,
+ // make sure to check the shadow with both dark and light themes.
+ //
+ // After making the graphics, I cut out the top right, bottom left
+ // and bottom right corners as 20x20 images, and these are reproduced by
+ // painting them in the corresponding places in the target graphics context.
+ // I then grabbed a single horizontal gradient line from the middle of the
+ // right edge,and a single vertical gradient line from the bottom. These
+ // are then painted scaled/stretched in the target to fill the gaps between
+ // the three corner images.
+ //
+ // Filenames: bl=bottom left, b=bottom, br=bottom right, r=right, tr=top right
+ sShadow2BottomLeft = readImage("shadow2-bl.png"); //$NON-NLS-1$
+ sShadow2Bottom = readImage("shadow2-b.png"); //$NON-NLS-1$
+ sShadow2BottomRight = readImage("shadow2-br.png"); //$NON-NLS-1$
+ sShadow2Right = readImage("shadow2-r.png"); //$NON-NLS-1$
+ sShadow2TopRight = readImage("shadow2-tr.png"); //$NON-NLS-1$
+ assert sShadow2BottomLeft != null;
+ assert sShadow2TopRight != null;
+ assert sShadow2BottomRight.getWidth() == SMALL_SHADOW_SIZE;
+ assert sShadow2BottomRight.getHeight() == SMALL_SHADOW_SIZE;
+ }
+
+ int blWidth = sShadow2BottomLeft.getWidth();
+ int trHeight = sShadow2TopRight.getHeight();
+ if (width < blWidth) {
+ return;
+ }
+ if (height < trHeight) {
+ return;
+ }
+
+ gc.drawImage(sShadow2BottomLeft, x, y + height, null);
+ gc.drawImage(sShadow2BottomRight, x + width, y + height, null);
+ gc.drawImage(sShadow2TopRight, x + width, y, null);
+ gc.drawImage(sShadow2Bottom,
+ x + sShadow2BottomLeft.getWidth(), y + height,
+ x + width, y + height + sShadow2Bottom.getHeight(),
+ 0, 0, sShadow2Bottom.getWidth(), sShadow2Bottom.getHeight(),
+ null);
+ gc.drawImage(sShadow2Right,
+ x + width, y + sShadow2TopRight.getHeight(),
+ x + width + sShadow2Right.getWidth(), y + height,
+ 0, 0, sShadow2Right.getWidth(), sShadow2Right.getHeight(),
+ null);
+ }
+
+ /**
+ * Reads the given image from the plugin folder
+ *
+ * @param name the name of the image (including file extension)
+ * @return the corresponding image, or null if something goes wrong
+ */
+ @Nullable
+ public static BufferedImage readImage(@NonNull String name) {
+ InputStream stream = ImageUtils.class.getResourceAsStream("/icons/" + name); //$NON-NLS-1$
+ if (stream != null) {
+ try {
+ return ImageIO.read(stream);
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Could not read %1$s", name);
+ } finally {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ // Dumb API
+ }
+ }
+ }
+
+ return null;
+ }
+
+ // Normal drop shadow
+ private static BufferedImage sShadowBottomLeft;
+ private static BufferedImage sShadowBottom;
+ private static BufferedImage sShadowBottomRight;
+ private static BufferedImage sShadowRight;
+ private static BufferedImage sShadowTopRight;
+
+ // Small drop shadow
+ private static BufferedImage sShadow2BottomLeft;
+ private static BufferedImage sShadow2Bottom;
+ private static BufferedImage sShadow2BottomRight;
+ private static BufferedImage sShadow2Right;
+ private static BufferedImage sShadow2TopRight;
+
+ /**
+ * Returns a bounding rectangle for the given list of rectangles. If the list is
+ * empty, the bounding rectangle is null.
+ *
+ * @param items the list of rectangles to compute a bounding rectangle for (may not be
+ * null)
+ * @return a bounding rectangle of the passed in rectangles, or null if the list is
+ * empty
+ */
+ public static Rectangle getBoundingRectangle(List<Rectangle> items) {
+ Iterator<Rectangle> iterator = items.iterator();
+ if (!iterator.hasNext()) {
+ return null;
+ }
+
+ Rectangle bounds = iterator.next();
+ Rectangle union = new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height);
+ while (iterator.hasNext()) {
+ union.add(iterator.next());
+ }
+
+ return union;
+ }
+
+ /**
+ * Returns a new image which contains of the sub image given by the rectangle (x1,y1)
+ * to (x2,y2)
+ *
+ * @param source the source image
+ * @param x1 top left X coordinate
+ * @param y1 top left Y coordinate
+ * @param x2 bottom right X coordinate
+ * @param y2 bottom right Y coordinate
+ * @return a new image containing the pixels in the given range
+ */
+ public static BufferedImage subImage(BufferedImage source, int x1, int y1, int x2, int y2) {
+ int width = x2 - x1;
+ int height = y2 - y1;
+ int imageType = source.getType();
+ if (imageType == BufferedImage.TYPE_CUSTOM) {
+ imageType = BufferedImage.TYPE_INT_ARGB;
+ }
+ BufferedImage sub = new BufferedImage(width, height, imageType);
+ Graphics g = sub.getGraphics();
+ g.drawImage(source, 0, 0, width, height, x1, y1, x2, y2, null);
+ g.dispose();
+
+ return sub;
+ }
+
+ /**
+ * Returns the color value represented by the given string value
+ * @param value the color value
+ * @return the color as an int
+ * @throw NumberFormatException if the conversion failed.
+ */
+ public static int getColor(String value) {
+ // Copied from ResourceHelper in layoutlib
+ if (value != null) {
+ if (value.startsWith("#") == false) { //$NON-NLS-1$
+ throw new NumberFormatException(
+ String.format("Color value '%s' must start with #", value));
+ }
+
+ value = value.substring(1);
+
+ // make sure it's not longer than 32bit
+ if (value.length() > 8) {
+ throw new NumberFormatException(String.format(
+ "Color value '%s' is too long. Format is either" +
+ "#AARRGGBB, #RRGGBB, #RGB, or #ARGB",
+ value));
+ }
+
+ if (value.length() == 3) { // RGB format
+ char[] color = new char[8];
+ color[0] = color[1] = 'F';
+ color[2] = color[3] = value.charAt(0);
+ color[4] = color[5] = value.charAt(1);
+ color[6] = color[7] = value.charAt(2);
+ value = new String(color);
+ } else if (value.length() == 4) { // ARGB format
+ char[] color = new char[8];
+ color[0] = color[1] = value.charAt(0);
+ color[2] = color[3] = value.charAt(1);
+ color[4] = color[5] = value.charAt(2);
+ color[6] = color[7] = value.charAt(3);
+ value = new String(color);
+ } else if (value.length() == 6) {
+ value = "FF" + value; //$NON-NLS-1$
+ }
+
+ // this is a RRGGBB or AARRGGBB value
+
+ // Integer.parseInt will fail to parse strings like "ff191919", so we use
+ // a Long, but cast the result back into an int, since we know that we're only
+ // dealing with 32 bit values.
+ return (int)Long.parseLong(value, 16);
+ }
+
+ throw new NumberFormatException();
+ }
+
+ /**
+ * Resize the given image
+ *
+ * @param source the image to be scaled
+ * @param xScale x scale
+ * @param yScale y scale
+ * @return the scaled image
+ */
+ public static BufferedImage scale(BufferedImage source, double xScale, double yScale) {
+ return scale(source, xScale, yScale, 0, 0);
+ }
+
+ /**
+ * Resize the given image
+ *
+ * @param source the image to be scaled
+ * @param xScale x scale
+ * @param yScale y scale
+ * @param rightMargin extra margin to add on the right
+ * @param bottomMargin extra margin to add on the bottom
+ * @return the scaled image
+ */
+ public static BufferedImage scale(BufferedImage source, double xScale, double yScale,
+ int rightMargin, int bottomMargin) {
+ int sourceWidth = source.getWidth();
+ int sourceHeight = source.getHeight();
+ int destWidth = Math.max(1, (int) (xScale * sourceWidth));
+ int destHeight = Math.max(1, (int) (yScale * sourceHeight));
+ int imageType = source.getType();
+ if (imageType == BufferedImage.TYPE_CUSTOM) {
+ imageType = BufferedImage.TYPE_INT_ARGB;
+ }
+ if (xScale > 0.5 && yScale > 0.5) {
+ BufferedImage scaled =
+ new BufferedImage(destWidth + rightMargin, destHeight + bottomMargin, imageType);
+ Graphics2D g2 = scaled.createGraphics();
+ g2.setComposite(AlphaComposite.Src);
+ g2.setColor(new Color(0, true));
+ g2.fillRect(0, 0, destWidth + rightMargin, destHeight + bottomMargin);
+ g2.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR);
+ g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY);
+ g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
+ g2.drawImage(source, 0, 0, destWidth, destHeight, 0, 0, sourceWidth, sourceHeight,
+ null);
+ g2.dispose();
+ return scaled;
+ } else {
+ // When creating a thumbnail, using the above code doesn't work very well;
+ // you get some visible artifacts, especially for text. Instead use the
+ // technique of repeatedly scaling the image into half; this will cause
+ // proper averaging of neighboring pixels, and will typically (for the kinds
+ // of screen sizes used by this utility method in the layout editor) take
+ // about 3-4 iterations to get the result since we are logarithmically reducing
+ // the size. Besides, each successive pass in operating on much fewer pixels
+ // (a reduction of 4 in each pass).
+ //
+ // However, we may not be resizing to a size that can be reached exactly by
+ // successively diving in half. Therefore, once we're within a factor of 2 of
+ // the final size, we can do a resize to the exact target size.
+ // However, we can get even better results if we perform this final resize
+ // up front. Let's say we're going from width 1000 to a destination width of 85.
+ // The first approach would cause a resize from 1000 to 500 to 250 to 125, and
+ // then a resize from 125 to 85. That last resize can distort/blur a lot.
+ // Instead, we can start with the destination width, 85, and double it
+ // successfully until we're close to the initial size: 85, then 170,
+ // then 340, and finally 680. (The next one, 1360, is larger than 1000).
+ // So, now we *start* the thumbnail operation by resizing from width 1000 to
+ // width 680, which will preserve a lot of visual details such as text.
+ // Then we can successively resize the image in half, 680 to 340 to 170 to 85.
+ // We end up with the expected final size, but we've been doing an exact
+ // divide-in-half resizing operation at the end so there is less distortion.
+
+
+ int iterations = 0; // Number of halving operations to perform after the initial resize
+ int nearestWidth = destWidth; // Width closest to source width that = 2^x, x is integer
+ int nearestHeight = destHeight;
+ while (nearestWidth < sourceWidth / 2) {
+ nearestWidth *= 2;
+ nearestHeight *= 2;
+ iterations++;
+ }
+
+ // If we're supposed to add in margins, we need to do it in the initial resizing
+ // operation if we don't have any subsequent resizing operations.
+ if (iterations == 0) {
+ nearestWidth += rightMargin;
+ nearestHeight += bottomMargin;
+ }
+
+ BufferedImage scaled = new BufferedImage(nearestWidth, nearestHeight, imageType);
+ Graphics2D g2 = scaled.createGraphics();
+ g2.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR);
+ g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY);
+ g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
+ g2.drawImage(source, 0, 0, nearestWidth, nearestHeight,
+ 0, 0, sourceWidth, sourceHeight, null);
+ g2.dispose();
+
+ sourceWidth = nearestWidth;
+ sourceHeight = nearestHeight;
+ source = scaled;
+
+ for (int iteration = iterations - 1; iteration >= 0; iteration--) {
+ int halfWidth = sourceWidth / 2;
+ int halfHeight = sourceHeight / 2;
+ if (iteration == 0) { // Last iteration: Add margins in final image
+ scaled = new BufferedImage(halfWidth + rightMargin, halfHeight + bottomMargin,
+ imageType);
+ } else {
+ scaled = new BufferedImage(halfWidth, halfHeight, imageType);
+ }
+ g2 = scaled.createGraphics();
+ g2.setRenderingHint(KEY_INTERPOLATION,VALUE_INTERPOLATION_BILINEAR);
+ g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY);
+ g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
+ g2.drawImage(source, 0, 0,
+ halfWidth, halfHeight, 0, 0,
+ sourceWidth, sourceHeight,
+ null);
+ g2.dispose();
+
+ sourceWidth = halfWidth;
+ sourceHeight = halfHeight;
+ source = scaled;
+ iterations--;
+ }
+ return scaled;
+ }
+ }
+
+ /**
+ * Returns true if the given file path points to an image file recognized by
+ * Android. See http://developer.android.com/guide/appendix/media-formats.html
+ * for details.
+ *
+ * @param path the filename to be tested
+ * @return true if the file represents an image file
+ */
+ public static boolean hasImageExtension(String path) {
+ return endsWithIgnoreCase(path, DOT_PNG)
+ || endsWithIgnoreCase(path, DOT_9PNG)
+ || endsWithIgnoreCase(path, DOT_GIF)
+ || endsWithIgnoreCase(path, DOT_JPG)
+ || endsWithIgnoreCase(path, DOT_BMP);
+ }
+
+ /**
+ * Creates a new image of the given size filled with the given color
+ *
+ * @param width the width of the image
+ * @param height the height of the image
+ * @param color the color of the image
+ * @return a new image of the given size filled with the given color
+ */
+ public static BufferedImage createColoredImage(int width, int height, RGB color) {
+ BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ Graphics g = image.getGraphics();
+ g.setColor(new Color(color.red, color.green, color.blue));
+ g.fillRect(0, 0, image.getWidth(), image.getHeight());
+ g.dispose();
+ return image;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java
new file mode 100644
index 000000000..7bab914e5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java
@@ -0,0 +1,1111 @@
+/*
+ * Copyright (C) 2010 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 static com.android.SdkConstants.ATTR_LAYOUT;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.FD_RESOURCES;
+import static com.android.SdkConstants.FD_RES_LAYOUT;
+import static com.android.SdkConstants.TOOLS_URI;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+import static com.android.SdkConstants.VIEW_INCLUDE;
+import static com.android.ide.eclipse.adt.AdtConstants.WS_LAYOUTS;
+import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP;
+import static com.android.resources.ResourceType.LAYOUT;
+import static org.eclipse.core.resources.IResourceDelta.ADDED;
+import static org.eclipse.core.resources.IResourceDelta.CHANGED;
+import static org.eclipse.core.resources.IResourceDelta.CONTENT;
+import static org.eclipse.core.resources.IResourceDelta.REMOVED;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.resources.ResourceFile;
+import com.android.ide.common.resources.ResourceFolder;
+import com.android.ide.common.resources.ResourceItem;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager.IResourceListener;
+import com.android.ide.eclipse.adt.io.IFileWrapper;
+import com.android.io.IAbstractFile;
+import com.android.resources.ResourceType;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The include finder finds other XML files that are including a given XML file, and does
+ * so efficiently (caching results across IDE sessions etc).
+ */
+@SuppressWarnings("restriction") // XML model
+public class IncludeFinder {
+ /** Qualified name for the per-project persistent property include-map */
+ private final static QualifiedName CONFIG_INCLUDES = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ "includes");//$NON-NLS-1$
+
+ /**
+ * Qualified name for the per-project non-persistent property storing the
+ * {@link IncludeFinder} for this project
+ */
+ private final static QualifiedName INCLUDE_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ "includefinder"); //$NON-NLS-1$
+
+ /** Project that the include finder locates includes for */
+ private final IProject mProject;
+
+ /** Map from a layout resource name to a set of layouts included by the given resource */
+ private Map<String, List<String>> mIncludes = null;
+
+ /**
+ * Reverse map of {@link #mIncludes}; points to other layouts that are including a
+ * given layouts
+ */
+ private Map<String, List<String>> mIncludedBy = null;
+
+ /** Flag set during a refresh; ignore updates when this is true */
+ private static boolean sRefreshing;
+
+ /** Global (cross-project) resource listener */
+ private static ResourceListener sListener;
+
+ /**
+ * Constructs an {@link IncludeFinder} for the given project. Don't use this method;
+ * use the {@link #get} factory method instead.
+ *
+ * @param project project to create an {@link IncludeFinder} for
+ */
+ private IncludeFinder(IProject project) {
+ mProject = project;
+ }
+
+ /**
+ * Returns the {@link IncludeFinder} for the given project
+ *
+ * @param project the project the finder is associated with
+ * @return an {@link IncludeFinder} for the given project, never null
+ */
+ @NonNull
+ public static IncludeFinder get(IProject project) {
+ IncludeFinder finder = null;
+ try {
+ finder = (IncludeFinder) project.getSessionProperty(INCLUDE_FINDER);
+ } catch (CoreException e) {
+ // Not a problem; we will just create a new one
+ }
+
+ if (finder == null) {
+ finder = new IncludeFinder(project);
+ try {
+ project.setSessionProperty(INCLUDE_FINDER, finder);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't store IncludeFinder");
+ }
+ }
+
+ return finder;
+ }
+
+ /**
+ * Returns a list of resource names that are included by the given resource
+ *
+ * @param includer the resource name to return included layouts for
+ * @return the layouts included by the given resource
+ */
+ private List<String> getIncludesFrom(String includer) {
+ ensureInitialized();
+
+ return mIncludes.get(includer);
+ }
+
+ /**
+ * Gets the list of all other layouts that are including the given layout.
+ *
+ * @param included the file that is included
+ * @return the files that are including the given file, or null or empty
+ */
+ @Nullable
+ public List<Reference> getIncludedBy(IResource included) {
+ ensureInitialized();
+ String mapKey = getMapKey(included);
+ List<String> result = mIncludedBy.get(mapKey);
+ if (result == null) {
+ String name = getResourceName(included);
+ if (!name.equals(mapKey)) {
+ result = mIncludedBy.get(name);
+ }
+ }
+
+ if (result != null && result.size() > 0) {
+ List<Reference> references = new ArrayList<Reference>(result.size());
+ for (String s : result) {
+ references.add(new Reference(mProject, s));
+ }
+ return references;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns true if the given resource is included from some other layout in the
+ * project
+ *
+ * @param included the resource to check
+ * @return true if the file is included by some other layout
+ */
+ public boolean isIncluded(IResource included) {
+ ensureInitialized();
+ String mapKey = getMapKey(included);
+ List<String> result = mIncludedBy.get(mapKey);
+ if (result == null) {
+ String name = getResourceName(included);
+ if (!name.equals(mapKey)) {
+ result = mIncludedBy.get(name);
+ }
+ }
+
+ return result != null && result.size() > 0;
+ }
+
+ @VisibleForTesting
+ /* package */ List<String> getIncludedBy(String included) {
+ ensureInitialized();
+ return mIncludedBy.get(included);
+ }
+
+ /** Initialize the inclusion data structures, if not already done */
+ private void ensureInitialized() {
+ if (mIncludes == null) {
+ // Initialize
+ if (!readSettings()) {
+ // Couldn't read settings: probably the first time this code is running
+ // so there is no known data about includes.
+
+ // Yes, these should be multimaps! If we start using Guava replace
+ // these with multimaps.
+ mIncludes = new HashMap<String, List<String>>();
+ mIncludedBy = new HashMap<String, List<String>>();
+
+ scanProject();
+ saveSettings();
+ }
+ }
+ }
+
+ // ----- Persistence -----
+
+ /**
+ * Create a String serialization of the includes map. The map attempts to be compact;
+ * it strips out the @layout/ prefix, and eliminates the values for empty string
+ * values. The map can be restored by calling {@link #decodeMap}. The encoded String
+ * will have sorted keys.
+ *
+ * @param map the map to be serialized
+ * @return a serialization (never null) of the given map
+ */
+ @VisibleForTesting
+ public static String encodeMap(Map<String, List<String>> map) {
+ StringBuilder sb = new StringBuilder();
+
+ if (map != null) {
+ // Process the keys in sorted order rather than just
+ // iterating over the entry set to ensure stable output
+ List<String> keys = new ArrayList<String>(map.keySet());
+ Collections.sort(keys);
+ for (String key : keys) {
+ List<String> values = map.get(key);
+
+ if (sb.length() > 0) {
+ sb.append(',');
+ }
+ sb.append(key);
+ if (values.size() > 0) {
+ sb.append('=').append('>');
+ sb.append('{');
+ boolean first = true;
+ for (String value : values) {
+ if (first) {
+ first = false;
+ } else {
+ sb.append(',');
+ }
+ sb.append(value);
+ }
+ sb.append('}');
+ }
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Decodes the encoding (produced by {@link #encodeMap}) back into the original map,
+ * modulo any key sorting differences.
+ *
+ * @param encoded an encoding of a map created by {@link #encodeMap}
+ * @return a map corresponding to the encoded values, never null
+ */
+ @VisibleForTesting
+ public static Map<String, List<String>> decodeMap(String encoded) {
+ HashMap<String, List<String>> map = new HashMap<String, List<String>>();
+
+ if (encoded.length() > 0) {
+ int i = 0;
+ int end = encoded.length();
+
+ while (i < end) {
+
+ // Find key range
+ int keyBegin = i;
+ int keyEnd = i;
+ while (i < end) {
+ char c = encoded.charAt(i);
+ if (c == ',') {
+ break;
+ } else if (c == '=') {
+ i += 2; // Skip =>
+ break;
+ }
+ i++;
+ keyEnd = i;
+ }
+
+ List<String> values = new ArrayList<String>();
+ // Find values
+ if (i < end && encoded.charAt(i) == '{') {
+ i++;
+ while (i < end) {
+ int valueBegin = i;
+ int valueEnd = i;
+ char c = 0;
+ while (i < end) {
+ c = encoded.charAt(i);
+ if (c == ',' || c == '}') {
+ valueEnd = i;
+ break;
+ }
+ i++;
+ }
+ if (valueEnd > valueBegin) {
+ values.add(encoded.substring(valueBegin, valueEnd));
+ }
+
+ if (c == '}') {
+ if (i < end-1 && encoded.charAt(i+1) == ',') {
+ i++;
+ }
+ break;
+ }
+ assert c == ',';
+ i++;
+ }
+ }
+
+ String key = encoded.substring(keyBegin, keyEnd);
+ map.put(key, values);
+ i++;
+ }
+ }
+
+ return map;
+ }
+
+ /**
+ * Stores the settings in the persistent project storage.
+ */
+ private void saveSettings() {
+ // Serialize the mIncludes map into a compact String. The mIncludedBy map can be
+ // inferred from it.
+ String encoded = encodeMap(mIncludes);
+
+ try {
+ if (encoded.length() >= 2048) {
+ // The maximum length of a setting key is 2KB, according to the javadoc
+ // for the project class. It's unlikely that we'll
+ // hit this -- even with an average layout root name of 20 characters
+ // we can still store over a hundred names. But JUST IN CASE we run
+ // into this, we'll clear out the key in this name which means that the
+ // information will need to be recomputed in the next IDE session.
+ mProject.setPersistentProperty(CONFIG_INCLUDES, null);
+ } else {
+ String existing = mProject.getPersistentProperty(CONFIG_INCLUDES);
+ if (!encoded.equals(existing)) {
+ mProject.setPersistentProperty(CONFIG_INCLUDES, encoded);
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't store include settings");
+ }
+ }
+
+ /**
+ * Reads previously stored settings from the persistent project storage
+ *
+ * @return true iff settings were restored from the project
+ */
+ private boolean readSettings() {
+ try {
+ String encoded = mProject.getPersistentProperty(CONFIG_INCLUDES);
+ if (encoded != null) {
+ mIncludes = decodeMap(encoded);
+
+ // Set up a reverse map, pointing from included files to the files that
+ // included them
+ mIncludedBy = new HashMap<String, List<String>>(2 * mIncludes.size());
+ for (Map.Entry<String, List<String>> entry : mIncludes.entrySet()) {
+ // File containing the <include>
+ String includer = entry.getKey();
+ // Files being <include>'ed by the above file
+ List<String> included = entry.getValue();
+ setIncludedBy(includer, included);
+ }
+
+ return true;
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't read include settings");
+ }
+
+ return false;
+ }
+
+ // ----- File scanning -----
+
+ /**
+ * Scan the whole project for XML layout resources that are performing includes.
+ */
+ private void scanProject() {
+ ProjectResources resources = ResourceManager.getInstance().getProjectResources(mProject);
+ if (resources != null) {
+ Collection<ResourceItem> layouts = resources.getResourceItemsOfType(LAYOUT);
+ for (ResourceItem layout : layouts) {
+ List<ResourceFile> sources = layout.getSourceFileList();
+ for (ResourceFile source : sources) {
+ updateFileIncludes(source, false);
+ }
+ }
+
+ return;
+ }
+ }
+
+ /**
+ * Scans the given {@link ResourceFile} and if it is a layout resource, updates the
+ * includes in it.
+ *
+ * @param resourceFile the {@link ResourceFile} to be scanned for includes (doesn't
+ * have to be only layout XML files; this method will filter the type)
+ * @param singleUpdate true if this is a single file being updated, false otherwise
+ * (e.g. during initial project scanning)
+ * @return true if we updated the includes for the resource file
+ */
+ private boolean updateFileIncludes(ResourceFile resourceFile, boolean singleUpdate) {
+ Collection<ResourceType> resourceTypes = resourceFile.getResourceTypes();
+ for (ResourceType type : resourceTypes) {
+ if (type == ResourceType.LAYOUT) {
+ ensureInitialized();
+
+ List<String> includes = Collections.emptyList();
+ if (resourceFile.getFile() instanceof IFileWrapper) {
+ IFile file = ((IFileWrapper) resourceFile.getFile()).getIFile();
+
+ // See if we have an existing XML model for this file; if so, we can
+ // just look directly at the parse tree
+ boolean hadXmlModel = false;
+ IStructuredModel model = null;
+ try {
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ model = modelManager.getExistingModelForRead(file);
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ Document document = domModel.getDocument();
+ includes = findIncludesInDocument(document);
+ hadXmlModel = true;
+ }
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ // If no XML model we have to read the XML contents and (possibly) parse it.
+ // The actual file may not exist anymore (e.g. when deleting a layout file
+ // or when the workspace is out of sync.)
+ if (!hadXmlModel) {
+ String xml = AdtPlugin.readFile(file);
+ if (xml != null) {
+ includes = findIncludes(xml);
+ }
+ }
+ } else {
+ String xml = AdtPlugin.readFile(resourceFile);
+ if (xml != null) {
+ includes = findIncludes(xml);
+ }
+ }
+
+ String key = getMapKey(resourceFile);
+ if (includes.equals(getIncludesFrom(key))) {
+ // Common case -- so avoid doing settings flush etc
+ return false;
+ }
+
+ boolean detectCycles = singleUpdate;
+ setIncluded(key, includes, detectCycles);
+
+ if (singleUpdate) {
+ saveSettings();
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Finds the list of includes in the given XML content. It attempts quickly return
+ * empty if the file does not include any include tags; it does this by only parsing
+ * if it detects the string &lt;include in the file.
+ */
+ @VisibleForTesting
+ @NonNull
+ static List<String> findIncludes(@NonNull String xml) {
+ int index = xml.indexOf(ATTR_LAYOUT);
+ if (index != -1) {
+ return findIncludesInXml(xml);
+ }
+
+ return Collections.emptyList();
+ }
+
+ /**
+ * Parses the given XML content and extracts all the included URLs and returns them
+ *
+ * @param xml layout XML content to be parsed for includes
+ * @return a list of included urls, or null
+ */
+ @VisibleForTesting
+ @NonNull
+ static List<String> findIncludesInXml(@NonNull String xml) {
+ Document document = DomUtilities.parseDocument(xml, false /*logParserErrors*/);
+ if (document != null) {
+ return findIncludesInDocument(document);
+ }
+
+ return Collections.emptyList();
+ }
+
+ /** Searches the given DOM document and returns the list of includes, if any */
+ @NonNull
+ private static List<String> findIncludesInDocument(@NonNull Document document) {
+ List<String> includes = findIncludesInDocument(document, null);
+ if (includes == null) {
+ includes = Collections.emptyList();
+ }
+ return includes;
+ }
+
+ @Nullable
+ private static List<String> findIncludesInDocument(@NonNull Node node,
+ @Nullable List<String> urls) {
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ String tag = node.getNodeName();
+ boolean isInclude = tag.equals(VIEW_INCLUDE);
+ boolean isFragment = tag.equals(VIEW_FRAGMENT);
+ if (isInclude || isFragment) {
+ Element element = (Element) node;
+ String url;
+ if (isInclude) {
+ url = element.getAttribute(ATTR_LAYOUT);
+ } else {
+ url = element.getAttributeNS(TOOLS_URI, ATTR_LAYOUT);
+ }
+ if (url.length() > 0) {
+ String resourceName = urlToLocalResource(url);
+ if (resourceName != null) {
+ if (urls == null) {
+ urls = new ArrayList<String>();
+ }
+ urls.add(resourceName);
+ }
+ }
+
+ }
+ }
+
+ NodeList children = node.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ urls = findIncludesInDocument(children.item(i), urls);
+ }
+
+ return urls;
+ }
+
+
+ /**
+ * Returns the layout URL to a local resource name (provided the URL is a local
+ * resource, not something in @android etc.) Returns null otherwise.
+ */
+ private static String urlToLocalResource(String url) {
+ if (!url.startsWith("@")) { //$NON-NLS-1$
+ return null;
+ }
+ int typeEnd = url.indexOf('/', 1);
+ if (typeEnd == -1) {
+ return null;
+ }
+ int nameBegin = typeEnd + 1;
+ int typeBegin = 1;
+ int colon = url.lastIndexOf(':', typeEnd);
+ if (colon != -1) {
+ String packageName = url.substring(typeBegin, colon);
+ if ("android".equals(packageName)) { //$NON-NLS-1$
+ // Don't want to point to non-local resources
+ return null;
+ }
+
+ typeBegin = colon + 1;
+ assert "layout".equals(url.substring(typeBegin, typeEnd)); //$NON-NLS-1$
+ }
+
+ return url.substring(nameBegin);
+ }
+
+ /**
+ * Record the list of included layouts from the given layout
+ *
+ * @param includer the layout including other layouts
+ * @param included the layouts that were included by the including layout
+ * @param detectCycles if true, check for cycles and report them as project errors
+ */
+ @VisibleForTesting
+ /* package */ void setIncluded(String includer, List<String> included, boolean detectCycles) {
+ // Remove previously linked inverse mappings
+ List<String> oldIncludes = mIncludes.get(includer);
+ if (oldIncludes != null && oldIncludes.size() > 0) {
+ for (String includee : oldIncludes) {
+ List<String> includers = mIncludedBy.get(includee);
+ if (includers != null) {
+ includers.remove(includer);
+ }
+ }
+ }
+
+ mIncludes.put(includer, included);
+ // Reverse mapping: for included items, point back to including file
+ setIncludedBy(includer, included);
+
+ if (detectCycles) {
+ detectCycles(includer);
+ }
+ }
+
+ /** Record the list of included layouts from the given layout */
+ private void setIncludedBy(String includer, List<String> included) {
+ for (String target : included) {
+ List<String> list = mIncludedBy.get(target);
+ if (list == null) {
+ list = new ArrayList<String>(2); // We don't expect many includes
+ mIncludedBy.put(target, list);
+ }
+ if (!list.contains(includer)) {
+ list.add(includer);
+ }
+ }
+ }
+
+ /** Start listening on project resources */
+ public static void start() {
+ assert sListener == null;
+ sListener = new ResourceListener();
+ ResourceManager.getInstance().addListener(sListener);
+ }
+
+ /** Stop listening on project resources */
+ public static void stop() {
+ assert sListener != null;
+ ResourceManager.getInstance().addListener(sListener);
+ }
+
+ private static String getMapKey(ResourceFile resourceFile) {
+ IAbstractFile file = resourceFile.getFile();
+ String name = file.getName();
+ String folderName = file.getParentFolder().getName();
+ return getMapKey(folderName, name);
+ }
+
+ private static String getMapKey(IResource resourceFile) {
+ String folderName = resourceFile.getParent().getName();
+ String name = resourceFile.getName();
+ return getMapKey(folderName, name);
+ }
+
+ private static String getResourceName(IResource resourceFile) {
+ String name = resourceFile.getName();
+ int baseEnd = name.length() - EXT_XML.length() - 1; // -1: the dot
+ if (baseEnd > 0) {
+ name = name.substring(0, baseEnd);
+ }
+
+ return name;
+ }
+
+ private static String getMapKey(String folderName, String name) {
+ int baseEnd = name.length() - EXT_XML.length() - 1; // -1: the dot
+ if (baseEnd > 0) {
+ name = name.substring(0, baseEnd);
+ }
+
+ // Create a map key for the given resource file
+ // This will map
+ // /res/layout/foo.xml => "foo"
+ // /res/layout-land/foo.xml => "-land/foo"
+
+ if (FD_RES_LAYOUT.equals(folderName)) {
+ // Normal case -- keep just the basename
+ return name;
+ } else {
+ // Store the relative path from res/ on down, so
+ // /res/layout-land/foo.xml becomes "layout-land/foo"
+ //if (folderName.startsWith(FD_LAYOUT)) {
+ // folderName = folderName.substring(FD_LAYOUT.length());
+ //}
+
+ return folderName + WS_SEP + name;
+ }
+ }
+
+ /** Listener of resource file saves, used to update layout inclusion data structures */
+ private static class ResourceListener implements IResourceListener {
+ @Override
+ public void fileChanged(IProject project, ResourceFile file, int eventType) {
+ if (sRefreshing) {
+ return;
+ }
+
+ if ((eventType & (CHANGED | ADDED | REMOVED | CONTENT)) == 0) {
+ return;
+ }
+
+ IncludeFinder finder = get(project);
+ if (finder != null) {
+ if (finder.updateFileIncludes(file, true)) {
+ finder.saveSettings();
+ }
+ }
+ }
+
+ @Override
+ public void folderChanged(IProject project, ResourceFolder folder, int eventType) {
+ // We only care about layout resource files
+ }
+ }
+
+ // ----- Cycle detection -----
+
+ private void detectCycles(String from) {
+ // Perform DFS on the include graph and look for a cycle; if we find one, produce
+ // a chain of includes on the way back to show to the user
+ if (mIncludes.size() > 0) {
+ Set<String> visiting = new HashSet<String>(mIncludes.size());
+ String chain = dfs(from, visiting);
+ if (chain != null) {
+ addError(from, chain);
+ } else {
+ // Is there an existing error for us to clean up?
+ removeErrors(from);
+ }
+ }
+ }
+
+ /** Format to chain include cycles in: a=>b=>c=>d etc */
+ private final String CHAIN_FORMAT = "%1$s=>%2$s"; //$NON-NLS-1$
+
+ private String dfs(String from, Set<String> visiting) {
+ visiting.add(from);
+
+ List<String> includes = mIncludes.get(from);
+ if (includes != null && includes.size() > 0) {
+ for (String include : includes) {
+ if (visiting.contains(include)) {
+ return String.format(CHAIN_FORMAT, from, include);
+ }
+ String chain = dfs(include, visiting);
+ if (chain != null) {
+ return String.format(CHAIN_FORMAT, from, chain);
+ }
+ }
+ }
+
+ visiting.remove(from);
+
+ return null;
+ }
+
+ private void removeErrors(String from) {
+ final IResource resource = findResource(from);
+ if (resource != null) {
+ try {
+ final String markerId = IMarker.PROBLEM;
+
+ IMarker[] markers = resource.findMarkers(markerId, true, IResource.DEPTH_ZERO);
+
+ for (final IMarker marker : markers) {
+ String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null);
+ if (tmpMsg == null || tmpMsg.startsWith(MESSAGE)) {
+ // Remove
+ runLater(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ sRefreshing = true;
+ marker.delete();
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Can't delete problem marker");
+ } finally {
+ sRefreshing = false;
+ }
+ }
+ });
+ }
+ }
+ } catch (CoreException e) {
+ // if we couldn't get the markers, then we just mark the file again
+ // (since markerAlreadyExists is initialized to false, we do nothing)
+ }
+ }
+ }
+
+ /** Error message for cycles */
+ private static final String MESSAGE = "Found cyclical <include> chain";
+
+ private void addError(String from, String chain) {
+ final IResource resource = findResource(from);
+ if (resource != null) {
+ final String markerId = IMarker.PROBLEM;
+ final String message = String.format("%1$s: %2$s", MESSAGE, chain);
+ final int lineNumber = 1;
+ final int severity = IMarker.SEVERITY_ERROR;
+
+ // check if there's a similar marker already, since aapt is launched twice
+ boolean markerAlreadyExists = false;
+ try {
+ IMarker[] markers = resource.findMarkers(markerId, true, IResource.DEPTH_ZERO);
+
+ for (IMarker marker : markers) {
+ int tmpLine = marker.getAttribute(IMarker.LINE_NUMBER, -1);
+ if (tmpLine != lineNumber) {
+ break;
+ }
+
+ int tmpSeverity = marker.getAttribute(IMarker.SEVERITY, -1);
+ if (tmpSeverity != severity) {
+ break;
+ }
+
+ String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null);
+ if (tmpMsg == null || tmpMsg.equals(message) == false) {
+ break;
+ }
+
+ // if we're here, all the marker attributes are equals, we found it
+ // and exit
+ markerAlreadyExists = true;
+ break;
+ }
+
+ } catch (CoreException e) {
+ // if we couldn't get the markers, then we just mark the file again
+ // (since markerAlreadyExists is initialized to false, we do nothing)
+ }
+
+ if (!markerAlreadyExists) {
+ runLater(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ sRefreshing = true;
+
+ // Adding a resource will force a refresh on the file;
+ // ignore these updates
+ BaseProjectHelper.markResource(resource, markerId, message, lineNumber,
+ severity);
+ } finally {
+ sRefreshing = false;
+ }
+ }
+ });
+ }
+ }
+ }
+
+ // FIXME: Find more standard Eclipse way to do this.
+ // We need to run marker registration/deletion "later", because when the include
+ // scanning is running it's in the middle of resource notification, so the IDE
+ // throws an exception
+ private static void runLater(Runnable runnable) {
+ Display display = Display.findDisplay(Thread.currentThread());
+ if (display != null) {
+ display.asyncExec(runnable);
+ } else {
+ AdtPlugin.log(IStatus.WARNING, "Could not find display");
+ }
+ }
+
+ /**
+ * Finds the project resource for the given layout path
+ *
+ * @param from the resource name
+ * @return the {@link IResource}, or null if not found
+ */
+ private IResource findResource(String from) {
+ final IResource resource = mProject.findMember(WS_LAYOUTS + WS_SEP + from + '.' + EXT_XML);
+ return resource;
+ }
+
+ /**
+ * Creates a blank, project-less {@link IncludeFinder} <b>for use by unit tests
+ * only</b>
+ */
+ @VisibleForTesting
+ /* package */ static IncludeFinder create() {
+ IncludeFinder finder = new IncludeFinder(null);
+ finder.mIncludes = new HashMap<String, List<String>>();
+ finder.mIncludedBy = new HashMap<String, List<String>>();
+ return finder;
+ }
+
+ /** A reference to a particular file in the project */
+ public static class Reference {
+ /** The unique id referencing the file, such as (for res/layout-land/main.xml)
+ * "layout-land/main") */
+ private final String mId;
+
+ /** The project containing the file */
+ private final IProject mProject;
+
+ /** The resource name of the file, such as (for res/layout/main.xml) "main" */
+ private String mName;
+
+ /** Creates a new include reference */
+ private Reference(IProject project, String id) {
+ super();
+ mProject = project;
+ mId = id;
+ }
+
+ /**
+ * Returns the id identifying the given file within the project
+ *
+ * @return the id identifying the given file within the project
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the {@link IFile} in the project for the given file. May return null if
+ * there is an error in locating the file or if the file no longer exists.
+ *
+ * @return the project file, or null
+ */
+ public IFile getFile() {
+ String reference = mId;
+ if (!reference.contains(WS_SEP)) {
+ reference = FD_RES_LAYOUT + WS_SEP + reference;
+ }
+
+ String projectPath = FD_RESOURCES + WS_SEP + reference + '.' + EXT_XML;
+ IResource member = mProject.findMember(projectPath);
+ if (member instanceof IFile) {
+ return (IFile) member;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a description of this reference, suitable to be shown to the user
+ *
+ * @return a display name for the reference
+ */
+ public String getDisplayName() {
+ // The ID is deliberately kept in a pretty user-readable format but we could
+ // consider prepending layout/ on ids that don't have it (to make the display
+ // more uniform) or ripping out all layout[-constraint] prefixes out and
+ // instead prepending @ etc.
+ return mId;
+ }
+
+ /**
+ * Returns the name of the reference, suitable for resource lookup. For example,
+ * for "res/layout/main.xml", as well as for "res/layout-land/main.xml", this
+ * would be "main".
+ *
+ * @return the resource name of the reference
+ */
+ public String getName() {
+ if (mName == null) {
+ mName = mId;
+ int index = mName.lastIndexOf(WS_SEP);
+ if (index != -1) {
+ mName = mName.substring(index + 1);
+ }
+ }
+
+ return mName;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((mId == null) ? 0 : mId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Reference other = (Reference) obj;
+ if (mId == null) {
+ if (other.mId != null)
+ return false;
+ } else if (!mId.equals(other.mId))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "Reference [getId()=" + getId() //$NON-NLS-1$
+ + ", getDisplayName()=" + getDisplayName() //$NON-NLS-1$
+ + ", getName()=" + getName() //$NON-NLS-1$
+ + ", getFile()=" + getFile() + "]"; //$NON-NLS-1$
+ }
+
+ /**
+ * Creates a reference to the given file
+ *
+ * @param file the file to create a reference for
+ * @return a reference to the given file
+ */
+ public static Reference create(IFile file) {
+ return new Reference(file.getProject(), getMapKey(file));
+ }
+
+ /**
+ * Returns the resource name of this layout, such as {@code @layout/foo}.
+ *
+ * @return the resource name
+ */
+ public String getResourceName() {
+ return '@' + FD_RES_LAYOUT + '/' + getName();
+ }
+ }
+
+ /**
+ * Returns a collection of layouts (expressed as resource names, such as
+ * {@code @layout/foo} which would be invalid includes in the given layout
+ * (because it would introduce a cycle)
+ *
+ * @param layout the layout file to check for cyclic dependencies from
+ * @return a collection of layout resources which cannot be included from
+ * the given layout, never null
+ */
+ public Collection<String> getInvalidIncludes(IFile layout) {
+ IProject project = layout.getProject();
+ Reference self = Reference.create(layout);
+
+ // Add anyone who transitively can reach this file via includes.
+ LinkedList<Reference> queue = new LinkedList<Reference>();
+ List<Reference> invalid = new ArrayList<Reference>();
+ queue.add(self);
+ invalid.add(self);
+ Set<String> seen = new HashSet<String>();
+ seen.add(self.getId());
+ while (!queue.isEmpty()) {
+ Reference reference = queue.removeFirst();
+ String refId = reference.getId();
+
+ // Look up both configuration specific includes as well as includes in the
+ // base versions
+ List<String> included = getIncludedBy(refId);
+ if (refId.indexOf('/') != -1) {
+ List<String> baseIncluded = getIncludedBy(reference.getName());
+ if (included == null) {
+ included = baseIncluded;
+ } else if (baseIncluded != null) {
+ included = new ArrayList<String>(included);
+ included.addAll(baseIncluded);
+ }
+ }
+
+ if (included != null && included.size() > 0) {
+ for (String id : included) {
+ if (!seen.contains(id)) {
+ seen.add(id);
+ Reference ref = new Reference(project, id);
+ invalid.add(ref);
+ queue.addLast(ref);
+ }
+ }
+ }
+ }
+
+ List<String> result = new ArrayList<String>();
+ for (Reference reference : invalid) {
+ result.add(reference.getResourceName());
+ }
+
+ return result;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java
new file mode 100644
index 000000000..81c03edd5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2010 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.annotations.VisibleForTesting;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * The {@link IncludeOverlay} class renders masks to -partially- hide everything outside
+ * an included file's own content. This overlay is in use when you are editing an included
+ * file shown within a different file's context (e.g. "Show In > other").
+ */
+public class IncludeOverlay extends Overlay {
+ /** Mask transparency - 0 is transparent, 255 is opaque */
+ private static final int MASK_TRANSPARENCY = 160;
+
+ /** The associated {@link LayoutCanvas}. */
+ private LayoutCanvas mCanvas;
+
+ /**
+ * Constructs an {@link IncludeOverlay} tied to the given canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} to paint the overlay over.
+ */
+ public IncludeOverlay(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ }
+
+ @Override
+ public void paint(GC gc) {
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ List<Rectangle> includedBounds = viewHierarchy.getIncludedBounds();
+ if (includedBounds == null || includedBounds.size() == 0) {
+ // We don't support multiple included children yet. When that works,
+ // this code should use a BSP tree to figure out which regions to paint
+ // to leave holes in the mask.
+ return;
+ }
+
+ Image image = mCanvas.getImageOverlay().getImage();
+ if (image == null) {
+ return;
+ }
+
+ int oldAlpha = gc.getAlpha();
+ gc.setAlpha(MASK_TRANSPARENCY);
+ Color bg = gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
+ gc.setBackground(bg);
+
+ CanvasViewInfo root = viewHierarchy.getRoot();
+ Rectangle whole = root.getAbsRect();
+ whole = new Rectangle(whole.x, whole.y, whole.width + 1, whole.height + 1);
+ Collection<Rectangle> masks = subtractRectangles(whole, includedBounds);
+
+ for (Rectangle mask : masks) {
+ ControlPoint topLeft = LayoutPoint.create(mCanvas, mask.x, mask.y).toControl();
+ ControlPoint bottomRight = LayoutPoint.create(mCanvas, mask.x + mask.width,
+ mask.y + mask.height).toControl();
+ int x1 = topLeft.x;
+ int y1 = topLeft.y;
+ int x2 = bottomRight.x;
+ int y2 = bottomRight.y;
+
+ gc.fillRectangle(x1, y1, x2 - x1, y2 - y1);
+ }
+
+ gc.setAlpha(oldAlpha);
+ }
+
+ /**
+ * Given a Rectangle, remove holes from it (specified as a collection of Rectangles) such
+ * that the result is a list of rectangles that cover everything that is not a hole.
+ *
+ * @param rectangle the rectangle to subtract from
+ * @param holes the holes to subtract from the rectangle
+ * @return a list of sub rectangles that remain after subtracting out the given list of holes
+ */
+ @VisibleForTesting
+ static Collection<Rectangle> subtractRectangles(
+ Rectangle rectangle, Collection<Rectangle> holes) {
+ List<Rectangle> result = new ArrayList<Rectangle>();
+ result.add(rectangle);
+
+ for (Rectangle hole : holes) {
+ List<Rectangle> tempResult = new ArrayList<Rectangle>();
+ for (Rectangle r : result) {
+ if (hole.intersects(r)) {
+ // Clip the hole to fit the rectangle bounds
+ Rectangle h = hole.intersection(r);
+
+ // Split the rectangle
+
+ // Above (includes the NW and NE corners)
+ if (h.y > r.y) {
+ tempResult.add(new Rectangle(r.x, r.y, r.width, h.y - r.y));
+ }
+
+ // Left (not including corners)
+ if (h.x > r.x) {
+ tempResult.add(new Rectangle(r.x, h.y, h.x - r.x, h.height));
+ }
+
+ int hx2 = h.x + h.width;
+ int hy2 = h.y + h.height;
+ int rx2 = r.x + r.width;
+ int ry2 = r.y + r.height;
+
+ // Below (includes the SW and SE corners)
+ if (hy2 < ry2) {
+ tempResult.add(new Rectangle(r.x, hy2, r.width, ry2 - hy2));
+ }
+
+ // Right (not including corners)
+ if (hx2 < rx2) {
+ tempResult.add(new Rectangle(hx2, h.y, rx2 - hx2, h.height));
+ }
+ } else {
+ tempResult.add(r);
+ }
+ }
+
+ result = tempResult;
+ }
+
+ return result;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java
new file mode 100644
index 000000000..1b1bd23c4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java
@@ -0,0 +1,732 @@
+/*
+ * Copyright (C) 2011 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 static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.api.RuleAction.Choices;
+import com.android.ide.common.api.RuleAction.Separator;
+import com.android.ide.common.api.RuleAction.Toggle;
+import com.android.ide.common.layout.BaseViewRule;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.Screen;
+import com.android.sdkuilib.internal.widgets.ResolutionChooserDialog;
+import com.google.common.base.Strings;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Toolbar shown at the top of the layout editor, which adds a number of context-sensitive
+ * layout actions (as well as zooming controls on the right).
+ */
+public class LayoutActionBar extends Composite {
+ private GraphicalEditorPart mEditor;
+ private ToolBar mLayoutToolBar;
+ private ToolBar mLintToolBar;
+ private ToolBar mZoomToolBar;
+ private ToolItem mZoomRealSizeButton;
+ private ToolItem mZoomOutButton;
+ private ToolItem mZoomResetButton;
+ private ToolItem mZoomInButton;
+ private ToolItem mZoomFitButton;
+ private ToolItem mLintButton;
+ private List<RuleAction> mPrevActions;
+
+ /**
+ * Creates a new {@link LayoutActionBar} and adds it to the given parent.
+ *
+ * @param parent the parent composite to add the actions bar to
+ * @param style the SWT style to apply
+ * @param editor the associated layout editor
+ */
+ public LayoutActionBar(Composite parent, int style, GraphicalEditorPart editor) {
+ super(parent, style | SWT.NO_FOCUS);
+ mEditor = editor;
+
+ GridLayout layout = new GridLayout(3, false);
+ setLayout(layout);
+
+ mLayoutToolBar = new ToolBar(this, /*SWT.WRAP |*/ SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL);
+ mLayoutToolBar.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false));
+ mZoomToolBar = createZoomControls();
+ mZoomToolBar.setLayoutData(new GridData(SWT.END, SWT.BEGINNING, false, false));
+ mLintToolBar = createLintControls();
+
+ GridData lintData = new GridData(SWT.END, SWT.BEGINNING, false, false);
+ lintData.exclude = true;
+ mLintToolBar.setLayoutData(lintData);
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ mPrevActions = null;
+ }
+
+ /** Updates the layout contents based on the current selection */
+ void updateSelection() {
+ NodeProxy parent = null;
+ LayoutCanvas canvas = mEditor.getCanvasControl();
+ SelectionManager selectionManager = canvas.getSelectionManager();
+ List<SelectionItem> selections = selectionManager.getSelections();
+ if (selections.size() > 0) {
+ // TODO: better handle multi-selection -- maybe we should disable it or
+ // something.
+ // What if you select children with different parents? Of different types?
+ // etc.
+ NodeProxy node = selections.get(0).getNode();
+ if (node != null && node.getParent() != null) {
+ parent = (NodeProxy) node.getParent();
+ }
+ }
+
+ if (parent == null) {
+ // Show the background's properties
+ CanvasViewInfo root = canvas.getViewHierarchy().getRoot();
+ if (root == null) {
+ return;
+ }
+ parent = canvas.getNodeFactory().create(root);
+ selections = Collections.emptyList();
+ }
+
+ RulesEngine engine = mEditor.getRulesEngine();
+ List<NodeProxy> selectedNodes = new ArrayList<NodeProxy>();
+ for (SelectionItem item : selections) {
+ selectedNodes.add(item.getNode());
+ }
+ List<RuleAction> actions = new ArrayList<RuleAction>();
+ engine.callAddLayoutActions(actions, parent, selectedNodes);
+
+ // Place actions in the correct order (the actions may come from different
+ // rules and should be merged properly via sorting keys)
+ Collections.sort(actions);
+
+ // Add in actions for the child as well, if there is exactly one.
+ // These are not merged into the parent list of actions; they are appended
+ // at the end.
+ int index = -1;
+ String label = null;
+ if (selectedNodes.size() == 1) {
+ List<RuleAction> itemActions = new ArrayList<RuleAction>();
+ NodeProxy selectedNode = selectedNodes.get(0);
+ engine.callAddLayoutActions(itemActions, selectedNode, null);
+ if (itemActions.size() > 0) {
+ Collections.sort(itemActions);
+
+ if (!(itemActions.get(0) instanceof RuleAction.Separator)) {
+ actions.add(RuleAction.createSeparator(0));
+ }
+ label = selectedNode.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (label != null) {
+ label = BaseViewRule.stripIdPrefix(label);
+ index = actions.size();
+ }
+ actions.addAll(itemActions);
+ }
+ }
+
+ if (!updateActions(actions)) {
+ updateToolbar(actions, index, label);
+ }
+ mPrevActions = actions;
+ }
+
+ /** Update the toolbar widgets */
+ private void updateToolbar(final List<RuleAction> actions, final int labelIndex,
+ final String label) {
+ if (mLayoutToolBar == null || mLayoutToolBar.isDisposed()) {
+ return;
+ }
+ for (ToolItem c : mLayoutToolBar.getItems()) {
+ c.dispose();
+ }
+ mLayoutToolBar.pack();
+ addActions(actions, labelIndex, label);
+ mLayoutToolBar.pack();
+ mLayoutToolBar.layout();
+ }
+
+ /**
+ * Attempts to update the existing toolbar actions, if the action list is
+ * similar to the current list. Returns false if this cannot be done and the
+ * contents must be replaced.
+ */
+ private boolean updateActions(@NonNull List<RuleAction> actions) {
+ List<RuleAction> before = mPrevActions;
+ List<RuleAction> after = actions;
+
+ if (before == null) {
+ return false;
+ }
+
+ if (!before.equals(after) || after.size() > mLayoutToolBar.getItemCount()) {
+ return false;
+ }
+
+ int actionIndex = 0;
+ for (int i = 0, max = mLayoutToolBar.getItemCount(); i < max; i++) {
+ ToolItem item = mLayoutToolBar.getItem(i);
+ int style = item.getStyle();
+ Object data = item.getData();
+ if (data != null) {
+ // One action can result in multiple toolbar items (e.g. a choice action
+ // can result in multiple radio buttons), so we've have to replace all of
+ // them with the corresponding new action
+ RuleAction prevAction = before.get(actionIndex);
+ while (prevAction != data) {
+ actionIndex++;
+ if (actionIndex == before.size()) {
+ return false;
+ }
+ prevAction = before.get(actionIndex);
+ if (prevAction == data) {
+ break;
+ } else if (!(prevAction instanceof RuleAction.Separator)) {
+ return false;
+ }
+ }
+ RuleAction newAction = after.get(actionIndex);
+ assert newAction.equals(prevAction); // Maybe I can do this lazily instead?
+
+ // Update action binding to the new action
+ item.setData(newAction);
+
+ // Sync button states: the checked state is not considered part of
+ // RuleAction equality
+ if ((style & SWT.CHECK) != 0) {
+ assert newAction instanceof Toggle;
+ Toggle toggle = (Toggle) newAction;
+ item.setSelection(toggle.isChecked());
+ } else if ((style & SWT.RADIO) != 0) {
+ assert newAction instanceof Choices;
+ Choices choices = (Choices) newAction;
+ String current = choices.getCurrent();
+ String id = (String) item.getData(ATTR_ID);
+ boolean selected = Strings.nullToEmpty(current).equals(id);
+ item.setSelection(selected);
+ }
+ } else {
+ // Must be a separator, or a label (which we insert for nested widgets)
+ assert (style & SWT.SEPARATOR) != 0 || !item.getText().isEmpty() : item;
+ }
+ }
+
+ return true;
+ }
+
+ private void addActions(List<RuleAction> actions, int labelIndex, String label) {
+ if (actions.size() > 0) {
+ // Flag used to indicate that if there are any actions -after- this, it
+ // should be separated from this current action (we don't unconditionally
+ // add a separator at the end of these groups in case there are no more
+ // actions at the end so that we don't have a trailing separator)
+ boolean needSeparator = false;
+
+ int index = 0;
+ for (RuleAction action : actions) {
+ if (index == labelIndex) {
+ final ToolItem button = new ToolItem(mLayoutToolBar, SWT.PUSH);
+ button.setText(label);
+ needSeparator = false;
+ }
+ index++;
+
+ if (action instanceof Separator) {
+ addSeparator(mLayoutToolBar);
+ needSeparator = false;
+ continue;
+ } else if (needSeparator) {
+ addSeparator(mLayoutToolBar);
+ needSeparator = false;
+ }
+
+ if (action instanceof RuleAction.Choices) {
+ RuleAction.Choices choices = (Choices) action;
+ if (!choices.isRadio()) {
+ addDropdown(choices);
+ } else {
+ addSeparator(mLayoutToolBar);
+ addRadio(choices);
+ needSeparator = true;
+ }
+ } else if (action instanceof RuleAction.Toggle) {
+ addToggle((Toggle) action);
+ } else {
+ addPlainAction(action);
+ }
+ }
+ }
+ }
+
+ /** Add a separator to the toolbar, unless there already is one there at the end already */
+ private static void addSeparator(ToolBar toolBar) {
+ int n = toolBar.getItemCount();
+ if (n > 0 && (toolBar.getItem(n - 1).getStyle() & SWT.SEPARATOR) == 0) {
+ ToolItem separator = new ToolItem(toolBar, SWT.SEPARATOR);
+ separator.setWidth(15);
+ }
+ }
+
+ private void addToggle(Toggle toggle) {
+ final ToolItem button = new ToolItem(mLayoutToolBar, SWT.CHECK);
+
+ URL iconUrl = toggle.getIconUrl();
+ String title = toggle.getTitle();
+ if (iconUrl != null) {
+ button.setImage(IconFactory.getInstance().getIcon(iconUrl));
+ button.setToolTipText(title);
+ } else {
+ button.setText(title);
+ }
+ button.setData(toggle);
+
+ button.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Toggle toggle = (Toggle) button.getData();
+ toggle.getCallback().action(toggle, getSelectedNodes(),
+ toggle.getId(), button.getSelection());
+ updateSelection();
+ }
+ });
+ if (toggle.isChecked()) {
+ button.setSelection(true);
+ }
+ }
+
+ private List<INode> getSelectedNodes() {
+ List<SelectionItem> selections =
+ mEditor.getCanvasControl().getSelectionManager().getSelections();
+ List<INode> nodes = new ArrayList<INode>(selections.size());
+ for (SelectionItem item : selections) {
+ nodes.add(item.getNode());
+ }
+
+ return nodes;
+ }
+
+
+ private void addPlainAction(RuleAction menuAction) {
+ final ToolItem button = new ToolItem(mLayoutToolBar, SWT.PUSH);
+
+ URL iconUrl = menuAction.getIconUrl();
+ String title = menuAction.getTitle();
+ if (iconUrl != null) {
+ button.setImage(IconFactory.getInstance().getIcon(iconUrl));
+ button.setToolTipText(title);
+ } else {
+ button.setText(title);
+ }
+ button.setData(menuAction);
+
+ button.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ RuleAction menuAction = (RuleAction) button.getData();
+ menuAction.getCallback().action(menuAction, getSelectedNodes(), menuAction.getId(),
+ false);
+ updateSelection();
+ }
+ });
+ }
+
+ private void addRadio(RuleAction.Choices choices) {
+ List<URL> icons = choices.getIconUrls();
+ List<String> titles = choices.getTitles();
+ List<String> ids = choices.getIds();
+ String current = choices.getCurrent() != null ? choices.getCurrent() : ""; //$NON-NLS-1$
+
+ assert icons != null;
+ assert icons.size() == titles.size();
+
+ for (int i = 0; i < icons.size(); i++) {
+ URL iconUrl = icons.get(i);
+ String title = titles.get(i);
+ final String id = ids.get(i);
+ final ToolItem item = new ToolItem(mLayoutToolBar, SWT.RADIO);
+ item.setToolTipText(title);
+ item.setImage(IconFactory.getInstance().getIcon(iconUrl));
+ item.setData(choices);
+ item.setData(ATTR_ID, id);
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (item.getSelection()) {
+ RuleAction.Choices choices = (Choices) item.getData();
+ choices.getCallback().action(choices, getSelectedNodes(), id, null);
+ updateSelection();
+ }
+ }
+ });
+ boolean selected = current.equals(id);
+ if (selected) {
+ item.setSelection(true);
+ }
+ }
+ }
+
+ private void addDropdown(RuleAction.Choices choices) {
+ final ToolItem combo = new ToolItem(mLayoutToolBar, SWT.DROP_DOWN);
+ URL iconUrl = choices.getIconUrl();
+ if (iconUrl != null) {
+ combo.setImage(IconFactory.getInstance().getIcon(iconUrl));
+ combo.setToolTipText(choices.getTitle());
+ } else {
+ combo.setText(choices.getTitle());
+ }
+ combo.setData(choices);
+
+ Listener menuListener = new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ Menu menu = new Menu(mLayoutToolBar.getShell(), SWT.POP_UP);
+ RuleAction.Choices choices = (Choices) combo.getData();
+ List<URL> icons = choices.getIconUrls();
+ List<String> titles = choices.getTitles();
+ List<String> ids = choices.getIds();
+ String current = choices.getCurrent() != null ? choices.getCurrent() : ""; //$NON-NLS-1$
+
+ for (int i = 0; i < titles.size(); i++) {
+ String title = titles.get(i);
+ final String id = ids.get(i);
+ URL itemIconUrl = icons != null && icons.size() > 0 ? icons.get(i) : null;
+ MenuItem item = new MenuItem(menu, SWT.CHECK);
+ item.setText(title);
+ if (itemIconUrl != null) {
+ Image itemIcon = IconFactory.getInstance().getIcon(itemIconUrl);
+ item.setImage(itemIcon);
+ }
+
+ boolean selected = id.equals(current);
+ if (selected) {
+ item.setSelection(true);
+ }
+
+ item.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ RuleAction.Choices choices = (Choices) combo.getData();
+ choices.getCallback().action(choices, getSelectedNodes(), id, null);
+ updateSelection();
+ }
+ });
+ }
+
+ Rectangle bounds = combo.getBounds();
+ Point location = new Point(bounds.x, bounds.y + bounds.height);
+ location = combo.getParent().toDisplay(location);
+ menu.setLocation(location.x, location.y);
+ menu.setVisible(true);
+ }
+ };
+ combo.addListener(SWT.Selection, menuListener);
+ }
+
+ // ---- Zoom Controls ----
+
+ @SuppressWarnings("unused") // SWT constructors have side effects, they are not unused
+ private ToolBar createZoomControls() {
+ ToolBar toolBar = new ToolBar(this, SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL);
+
+ IconFactory iconFactory = IconFactory.getInstance();
+ mZoomRealSizeButton = new ToolItem(toolBar, SWT.CHECK);
+ mZoomRealSizeButton.setToolTipText("Emulate Real Size");
+ mZoomRealSizeButton.setImage(iconFactory.getIcon("zoomreal")); //$NON-NLS-1$);
+ mZoomRealSizeButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ boolean newState = mZoomRealSizeButton.getSelection();
+ if (rescaleToReal(newState)) {
+ mZoomOutButton.setEnabled(!newState);
+ mZoomResetButton.setEnabled(!newState);
+ mZoomInButton.setEnabled(!newState);
+ mZoomFitButton.setEnabled(!newState);
+ } else {
+ mZoomRealSizeButton.setSelection(!newState);
+ }
+ }
+ });
+
+ mZoomFitButton = new ToolItem(toolBar, SWT.PUSH);
+ mZoomFitButton.setToolTipText("Zoom to Fit (0)");
+ mZoomFitButton.setImage(iconFactory.getIcon("zoomfit")); //$NON-NLS-1$);
+ mZoomFitButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ rescaleToFit(true);
+ }
+ });
+
+ mZoomResetButton = new ToolItem(toolBar, SWT.PUSH);
+ mZoomResetButton.setToolTipText("Reset Zoom to 100% (1)");
+ mZoomResetButton.setImage(iconFactory.getIcon("zoom100")); //$NON-NLS-1$);
+ mZoomResetButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ resetScale();
+ }
+ });
+
+ // Group zoom in/out separately
+ new ToolItem(toolBar, SWT.SEPARATOR);
+
+ mZoomOutButton = new ToolItem(toolBar, SWT.PUSH);
+ mZoomOutButton.setToolTipText("Zoom Out (-)");
+ mZoomOutButton.setImage(iconFactory.getIcon("zoomminus")); //$NON-NLS-1$);
+ mZoomOutButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ rescale(-1);
+ }
+ });
+
+ mZoomInButton = new ToolItem(toolBar, SWT.PUSH);
+ mZoomInButton.setToolTipText("Zoom In (+)");
+ mZoomInButton.setImage(iconFactory.getIcon("zoomplus")); //$NON-NLS-1$);
+ mZoomInButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ rescale(+1);
+ }
+ });
+
+ return toolBar;
+ }
+
+ @SuppressWarnings("unused") // SWT constructors have side effects, they are not unused
+ private ToolBar createLintControls() {
+ ToolBar toolBar = new ToolBar(this, SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL);
+
+ // Separate from adjacent toolbar
+ new ToolItem(toolBar, SWT.SEPARATOR);
+
+ ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
+ mLintButton = new ToolItem(toolBar, SWT.PUSH);
+ mLintButton.setToolTipText("Show Lint Warnings for this Layout");
+ mLintButton.setImage(sharedImages.getImage(ISharedImages.IMG_OBJS_WARN_TSK));
+ mLintButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ CommonXmlEditor editor = mEditor.getEditorDelegate().getEditor();
+ IFile file = editor.getInputFile();
+ if (file != null) {
+ EclipseLintClient.showErrors(getShell(), file, editor);
+ }
+ }
+ });
+
+ return toolBar;
+ }
+
+ /**
+ * Updates the lint indicator state in the given layout editor
+ */
+ public void updateErrorIndicator() {
+ updateErrorIndicator(mEditor.getEditedFile());
+ }
+
+ /**
+ * Updates the lint indicator state for the given file
+ *
+ * @param file the file to show the indicator status for
+ */
+ public void updateErrorIndicator(IFile file) {
+ IMarker[] markers = EclipseLintClient.getMarkers(file);
+ updateErrorIndicator(markers.length);
+ }
+
+ /**
+ * Sets whether the action bar should show the "lint warnings" button
+ *
+ * @param hasLintWarnings whether there are lint errors to be shown
+ */
+ private void updateErrorIndicator(final int markerCount) {
+ Display display = getDisplay();
+ if (display.getThread() != Thread.currentThread()) {
+ display.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (!isDisposed()) {
+ updateErrorIndicator(markerCount);
+ }
+ }
+ });
+ return;
+ }
+
+ GridData layoutData = (GridData) mLintToolBar.getLayoutData();
+ Integer existing = (Integer) mLintToolBar.getData();
+ Integer current = Integer.valueOf(markerCount);
+ if (!current.equals(existing)) {
+ mLintToolBar.setData(current);
+ boolean layout = false;
+ boolean hasLintWarnings = markerCount > 0 && AdtPrefs.getPrefs().isLintOnSave();
+ if (layoutData.exclude == hasLintWarnings) {
+ layoutData.exclude = !hasLintWarnings;
+ mLintToolBar.setVisible(hasLintWarnings);
+ layout = true;
+ }
+ if (markerCount > 0) {
+ String iconName = "";
+ switch (markerCount) {
+ case 1: iconName = "lint1"; break; //$NON-NLS-1$
+ case 2: iconName = "lint2"; break; //$NON-NLS-1$
+ case 3: iconName = "lint3"; break; //$NON-NLS-1$
+ case 4: iconName = "lint4"; break; //$NON-NLS-1$
+ case 5: iconName = "lint5"; break; //$NON-NLS-1$
+ case 6: iconName = "lint6"; break; //$NON-NLS-1$
+ case 7: iconName = "lint7"; break; //$NON-NLS-1$
+ case 8: iconName = "lint8"; break; //$NON-NLS-1$
+ case 9: iconName = "lint9"; break; //$NON-NLS-1$
+ default: iconName = "lint9p"; break;//$NON-NLS-1$
+ }
+ mLintButton.setImage(IconFactory.getInstance().getIcon(iconName));
+ }
+ if (layout) {
+ layout();
+ }
+ redraw();
+ }
+ }
+
+ /**
+ * Returns true if zooming in/out/to-fit/etc is allowed (which is not the case while
+ * emulating real size)
+ *
+ * @return true if zooming is allowed
+ */
+ boolean isZoomingAllowed() {
+ return mZoomInButton.isEnabled();
+ }
+
+ boolean isZoomingRealSize() {
+ return mZoomRealSizeButton.getSelection();
+ }
+
+ /**
+ * Rescales canvas.
+ * @param direction +1 for zoom in, -1 for zoom out
+ */
+ void rescale(int direction) {
+ LayoutCanvas canvas = mEditor.getCanvasControl();
+ double s = canvas.getScale();
+
+ if (direction > 0) {
+ s = s * 1.2;
+ } else {
+ s = s / 1.2;
+ }
+
+ // Some operations are faster if the zoom is EXACTLY 1.0 rather than ALMOST 1.0.
+ // (This is because there is a fast-path when image copying and the scale is 1.0;
+ // in that case it does not have to do any scaling).
+ //
+ // If you zoom out 10 times and then back in 10 times, small rounding errors mean
+ // that you end up with a scale=1.0000000000000004. In the cases, when you get close
+ // to 1.0, just make the zoom an exact 1.0.
+ if (Math.abs(s-1.0) < 0.0001) {
+ s = 1.0;
+ }
+
+ canvas.setScale(s, true /*redraw*/);
+ }
+
+ /**
+ * Reset the canvas scale to 100%
+ */
+ void resetScale() {
+ mEditor.getCanvasControl().setScale(1, true /*redraw*/);
+ }
+
+ /**
+ * Reset the canvas scale to best fit (so content is as large as possible without scrollbars)
+ */
+ void rescaleToFit(boolean onlyZoomOut) {
+ mEditor.getCanvasControl().setFitScale(onlyZoomOut, true /*allowZoomIn*/);
+ }
+
+ boolean rescaleToReal(boolean real) {
+ if (real) {
+ return computeAndSetRealScale(true /*redraw*/);
+ } else {
+ // reset the scale to 100%
+ mEditor.getCanvasControl().setScale(1, true /*redraw*/);
+ return true;
+ }
+ }
+
+ boolean computeAndSetRealScale(boolean redraw) {
+ // compute average dpi of X and Y
+ ConfigurationChooser chooser = mEditor.getConfigurationChooser();
+ Configuration config = chooser.getConfiguration();
+ Device device = config.getDevice();
+ Screen screen = device.getDefaultHardware().getScreen();
+ double dpi = (screen.getXdpi() + screen.getYdpi()) / 2.;
+
+ // get the monitor dpi
+ float monitor = AdtPrefs.getPrefs().getMonitorDensity();
+ if (monitor == 0.f) {
+ ResolutionChooserDialog dialog = new ResolutionChooserDialog(chooser.getShell());
+ if (dialog.open() == Window.OK) {
+ monitor = dialog.getDensity();
+ AdtPrefs.getPrefs().setMonitorDensity(monitor);
+ } else {
+ return false;
+ }
+ }
+
+ mEditor.getCanvasControl().setScale(monitor / dpi, redraw);
+ return true;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java
new file mode 100644
index 000000000..814b82cec
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java
@@ -0,0 +1,1720 @@
+/*
+ * 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();
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvasViewer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvasViewer.java
new file mode 100644
index 000000000..e349a1cb0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvasViewer.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2010 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.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+
+import org.eclipse.core.runtime.ListenerList;
+import org.eclipse.jface.util.SafeRunnable;
+import org.eclipse.jface.viewers.IPostSelectionProvider;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+
+/**
+ * JFace {@link Viewer} wrapper around {@link LayoutCanvas}.
+ * <p/>
+ * The viewer is owned by {@link GraphicalEditorPart}.
+ * <p/>
+ * The viewer is an {@link ISelectionProvider} instance and is set as the
+ * site's main {@link ISelectionProvider} by the editor part. Consequently
+ * canvas' selection changes are broadcasted to anyone listening, which includes
+ * the part itself as well as the associated outline and property sheet pages.
+ */
+class LayoutCanvasViewer extends Viewer implements IPostSelectionProvider {
+
+ private LayoutCanvas mCanvas;
+ private final LayoutEditorDelegate mEditorDelegate;
+
+ public LayoutCanvasViewer(LayoutEditorDelegate editorDelegate,
+ RulesEngine rulesEngine,
+ Composite parent,
+ int style) {
+ mEditorDelegate = editorDelegate;
+ mCanvas = new LayoutCanvas(editorDelegate, rulesEngine, parent, style);
+
+ mCanvas.getSelectionManager().addSelectionChangedListener(mSelectionListener);
+ }
+
+ private ISelectionChangedListener mSelectionListener = new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ fireSelectionChanged(event);
+ firePostSelectionChanged(event);
+ }
+ };
+
+ @Override
+ public Control getControl() {
+ return mCanvas;
+ }
+
+ /**
+ * Returns the underlying {@link LayoutCanvas}.
+ * This is the same control as returned by {@link #getControl()} but clients
+ * have it already casted in the right type.
+ * <p/>
+ * This can never be null.
+ * @return The underlying {@link LayoutCanvas}.
+ */
+ public LayoutCanvas getCanvas() {
+ return mCanvas;
+ }
+
+ /**
+ * Returns the current layout editor's input.
+ */
+ @Override
+ public Object getInput() {
+ return mEditorDelegate.getEditor().getEditorInput();
+ }
+
+ /**
+ * Unused. We don't support switching the input.
+ */
+ @Override
+ public void setInput(Object input) {
+ }
+
+ /**
+ * Returns a new {@link TreeSelection} where each {@link TreePath} item
+ * is a {@link CanvasViewInfo}.
+ */
+ @Override
+ public ISelection getSelection() {
+ return mCanvas.getSelectionManager().getSelection();
+ }
+
+ /**
+ * Sets a new selection. <code>reveal</code> is ignored right now.
+ * <p/>
+ * The selection can be null, which is interpreted as an empty selection.
+ */
+ @Override
+ public void setSelection(ISelection selection, boolean reveal) {
+ if (mEditorDelegate.getEditor().getIgnoreXmlUpdate()) {
+ return;
+ }
+ mCanvas.getSelectionManager().setSelection(selection);
+ }
+
+ /** Unused. Refreshing is done solely by the owning {@link LayoutEditorDelegate}. */
+ @Override
+ public void refresh() {
+ // ignore
+ }
+
+ public void dispose() {
+ if (mSelectionListener != null) {
+ mCanvas.getSelectionManager().removeSelectionChangedListener(mSelectionListener);
+ }
+ if (mCanvas != null) {
+ mCanvas.dispose();
+ mCanvas = null;
+ }
+ }
+
+ // ---- Implements IPostSelectionProvider ----
+
+ private ListenerList mPostChangedListeners = new ListenerList();
+
+ @Override
+ public void addPostSelectionChangedListener(ISelectionChangedListener listener) {
+ mPostChangedListeners.add(listener);
+ }
+
+ @Override
+ public void removePostSelectionChangedListener(ISelectionChangedListener listener) {
+ mPostChangedListeners.remove(listener);
+ }
+
+ protected void firePostSelectionChanged(final SelectionChangedEvent event) {
+ Object[] listeners = mPostChangedListeners.getListeners();
+ for (int i = 0; i < listeners.length; i++) {
+ final ISelectionChangedListener l = (ISelectionChangedListener) listeners[i];
+ SafeRunnable.run(new SafeRunnable() {
+ @Override
+ public void run() {
+ l.selectionChanged(event);
+ }
+ });
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java
new file mode 100644
index 000000000..b79e3b0a1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java
@@ -0,0 +1,413 @@
+/*
+ * Copyright (C) 2011 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 static com.android.SdkConstants.ANDROID_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_NUM_COLUMNS;
+import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
+import static com.android.SdkConstants.GRID_VIEW;
+import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.TOOLS_URI;
+import static com.android.SdkConstants.VALUE_AUTO_FIT;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.api.AdapterBinding;
+import com.android.ide.common.rendering.api.DataBindingItem;
+import com.android.ide.common.rendering.api.ResourceReference;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.progress.WorkbenchJob;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Design-time metadata lookup for layouts, such as fragment and AdapterView bindings.
+ */
+public class LayoutMetadata {
+ /** The default layout to use for list items in expandable list views */
+ public static final String DEFAULT_EXPANDABLE_LIST_ITEM = "simple_expandable_list_item_2"; //$NON-NLS-1$
+ /** The default layout to use for list items in plain list views */
+ public static final String DEFAULT_LIST_ITEM = "simple_list_item_2"; //$NON-NLS-1$
+ /** The default layout to use for list items in spinners */
+ public static final String DEFAULT_SPINNER_ITEM = "simple_spinner_item"; //$NON-NLS-1$
+
+ /** The string to start metadata comments with */
+ private static final String COMMENT_PROLOGUE = " Preview: ";
+ /** The property key, included in comments, which references a list item layout */
+ public static final String KEY_LV_ITEM = "listitem"; //$NON-NLS-1$
+ /** The property key, included in comments, which references a list header layout */
+ public static final String KEY_LV_HEADER = "listheader"; //$NON-NLS-1$
+ /** The property key, included in comments, which references a list footer layout */
+ public static final String KEY_LV_FOOTER = "listfooter"; //$NON-NLS-1$
+ /** The property key, included in comments, which references a fragment layout to show */
+ public static final String KEY_FRAGMENT_LAYOUT = "layout"; //$NON-NLS-1$
+ // NOTE: If you add additional keys related to resources, make sure you update the
+ // ResourceRenameParticipant
+
+ /** Utility class, do not create instances */
+ private LayoutMetadata() {
+ }
+
+ /**
+ * Returns the given property specified in the <b>current</b> element being
+ * processed by the given pull parser.
+ *
+ * @param parser the pull parser, which must be in the middle of processing
+ * the target element
+ * @param name the property name to look up
+ * @return the property value, or null if not defined
+ */
+ @Nullable
+ public static String getProperty(@NonNull XmlPullParser parser, @NonNull String name) {
+ String value = parser.getAttributeValue(TOOLS_URI, name);
+ if (value != null && value.isEmpty()) {
+ value = null;
+ }
+
+ return value;
+ }
+
+ /**
+ * Clears the old metadata from the given node
+ *
+ * @param node the XML node to associate metadata with
+ * @deprecated this method clears metadata using the old comment-based style;
+ * should only be used for migration at this point
+ */
+ @Deprecated
+ public static void clearLegacyComment(Node node) {
+ NodeList children = node.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.COMMENT_NODE) {
+ String text = child.getNodeValue();
+ if (text.startsWith(COMMENT_PROLOGUE)) {
+ Node commentNode = child;
+ // Remove the comment, along with surrounding whitespace if applicable
+ Node previous = commentNode.getPreviousSibling();
+ if (previous != null && previous.getNodeType() == Node.TEXT_NODE) {
+ if (previous.getNodeValue().trim().length() == 0) {
+ node.removeChild(previous);
+ }
+ }
+ node.removeChild(commentNode);
+ Node first = node.getFirstChild();
+ if (first != null && first.getNextSibling() == null
+ && first.getNodeType() == Node.TEXT_NODE) {
+ if (first.getNodeValue().trim().length() == 0) {
+ node.removeChild(first);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the given property of the given DOM node, or null
+ *
+ * @param node the XML node to associate metadata with
+ * @param name the name of the property to look up
+ * @return the value stored with the given node and name, or null
+ */
+ @Nullable
+ public static String getProperty(
+ @NonNull Node node,
+ @NonNull String name) {
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) node;
+ String value = element.getAttributeNS(TOOLS_URI, name);
+ if (value != null && value.isEmpty()) {
+ value = null;
+ }
+
+ return value;
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the given property of the given DOM node to a given value, or if null clears
+ * the property.
+ *
+ * @param editor the editor associated with the property
+ * @param node the XML node to associate metadata with
+ * @param name the name of the property to set
+ * @param value the value to store for the given node and name, or null to remove it
+ */
+ public static void setProperty(
+ @NonNull final AndroidXmlEditor editor,
+ @NonNull final Node node,
+ @NonNull final String name,
+ @Nullable final String value) {
+ // Clear out the old metadata
+ clearLegacyComment(node);
+
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ final Element element = (Element) node;
+ final String undoLabel = "Bind View";
+ AdtUtils.setToolsAttribute(editor, element, undoLabel, name, value,
+ false /*reveal*/, false /*append*/);
+
+ // Also apply the same layout to any corresponding elements in other configurations
+ // of this layout.
+ final IFile file = editor.getInputFile();
+ if (file != null) {
+ final List<IFile> variations = AdtUtils.getResourceVariations(file, false);
+ if (variations.isEmpty()) {
+ return;
+ }
+ Display display = AdtPlugin.getDisplay();
+ WorkbenchJob job = new WorkbenchJob(display, "Update alternate views") {
+ @Override
+ public IStatus runInUIThread(IProgressMonitor monitor) {
+ for (IFile variation : variations) {
+ if (variation.equals(file)) {
+ continue;
+ }
+ try {
+ // If the corresponding file is open in the IDE, use the
+ // editor version instead
+ if (!AdtPrefs.getPrefs().isSharedLayoutEditor()) {
+ if (setPropertyInEditor(undoLabel, variation, element, name,
+ value)) {
+ return Status.OK_STATUS;
+ }
+ }
+
+ boolean old = editor.getIgnoreXmlUpdate();
+ try {
+ editor.setIgnoreXmlUpdate(true);
+ setPropertyInFile(undoLabel, variation, element, name, value);
+ } finally {
+ editor.setIgnoreXmlUpdate(old);
+ }
+ } catch (Exception e) {
+ AdtPlugin.log(e, variation.getFullPath().toOSString());
+ }
+ }
+ return Status.OK_STATUS;
+ }
+
+ };
+ job.setSystem(true);
+ job.schedule();
+ }
+ }
+ }
+
+ private static boolean setPropertyInEditor(
+ @NonNull String undoLabel,
+ @NonNull IFile variation,
+ @NonNull final Element equivalentElement,
+ @NonNull final String name,
+ @Nullable final String value) {
+ Collection<IEditorPart> editors =
+ AdtUtils.findEditorsFor(variation, false /*restore*/);
+ for (IEditorPart part : editors) {
+ AndroidXmlEditor editor = AdtUtils.getXmlEditor(part);
+ if (editor != null) {
+ Document doc = DomUtilities.getDocument(editor);
+ if (doc != null) {
+ Element element = DomUtilities.findCorresponding(equivalentElement, doc);
+ if (element != null) {
+ AdtUtils.setToolsAttribute(editor, element, undoLabel, name,
+ value, false /*reveal*/, false /*append*/);
+ if (part instanceof GraphicalEditorPart) {
+ GraphicalEditorPart g = (GraphicalEditorPart) part;
+ g.recomputeLayout();
+ g.getCanvasControl().redraw();
+ }
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean setPropertyInFile(
+ @NonNull String undoLabel,
+ @NonNull IFile variation,
+ @NonNull final Element element,
+ @NonNull final String name,
+ @Nullable final String value) {
+ Document doc = DomUtilities.getDocument(variation);
+ if (doc != null && element.getOwnerDocument() != doc) {
+ Element other = DomUtilities.findCorresponding(element, doc);
+ if (other != null) {
+ AdtUtils.setToolsAttribute(variation, other, undoLabel,
+ name, value, false);
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /** Strips out @layout/ or @android:layout/ from the given layout reference */
+ private static String stripLayoutPrefix(String layout) {
+ if (layout.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX)) {
+ layout = layout.substring(ANDROID_LAYOUT_RESOURCE_PREFIX.length());
+ } else if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) {
+ layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length());
+ }
+
+ return layout;
+ }
+
+ /**
+ * Creates an {@link AdapterBinding} for the given view object, or null if the user
+ * has not yet chosen a target layout to use for the given AdapterView.
+ *
+ * @param viewObject the view object to create an adapter binding for
+ * @param map a map containing tools attribute metadata
+ * @return a binding, or null
+ */
+ @Nullable
+ public static AdapterBinding getNodeBinding(
+ @Nullable Object viewObject,
+ @NonNull Map<String, String> map) {
+ String header = map.get(KEY_LV_HEADER);
+ String footer = map.get(KEY_LV_FOOTER);
+ String layout = map.get(KEY_LV_ITEM);
+ if (layout != null || header != null || footer != null) {
+ int count = 12;
+ return getNodeBinding(viewObject, header, footer, layout, count);
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates an {@link AdapterBinding} for the given view object, or null if the user
+ * has not yet chosen a target layout to use for the given AdapterView.
+ *
+ * @param viewObject the view object to create an adapter binding for
+ * @param uiNode the ui node corresponding to the view object
+ * @return a binding, or null
+ */
+ @Nullable
+ public static AdapterBinding getNodeBinding(
+ @Nullable Object viewObject,
+ @NonNull UiViewElementNode uiNode) {
+ Node xmlNode = uiNode.getXmlNode();
+
+ String header = getProperty(xmlNode, KEY_LV_HEADER);
+ String footer = getProperty(xmlNode, KEY_LV_FOOTER);
+ String layout = getProperty(xmlNode, KEY_LV_ITEM);
+ if (layout != null || header != null || footer != null) {
+ int count = 12;
+ // If we're dealing with a grid view, multiply the list item count
+ // by the number of columns to ensure we have enough items
+ if (xmlNode instanceof Element && xmlNode.getNodeName().endsWith(GRID_VIEW)) {
+ Element element = (Element) xmlNode;
+ String columns = element.getAttributeNS(ANDROID_URI, ATTR_NUM_COLUMNS);
+ int multiplier = 2;
+ if (columns != null && columns.length() > 0 &&
+ !columns.equals(VALUE_AUTO_FIT)) {
+ try {
+ int c = Integer.parseInt(columns);
+ if (c >= 1 && c <= 10) {
+ multiplier = c;
+ }
+ } catch (NumberFormatException nufe) {
+ // some unexpected numColumns value: just stick with 2 columns for
+ // preview purposes
+ }
+ }
+ count *= multiplier;
+ }
+
+ return getNodeBinding(viewObject, header, footer, layout, count);
+ }
+
+ return null;
+ }
+
+ private static AdapterBinding getNodeBinding(Object viewObject,
+ String header, String footer, String layout, int count) {
+ if (layout != null || header != null || footer != null) {
+ AdapterBinding binding = new AdapterBinding(count);
+
+ if (header != null) {
+ boolean isFramework = header.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX);
+ binding.addHeader(new ResourceReference(stripLayoutPrefix(header),
+ isFramework));
+ }
+
+ if (footer != null) {
+ boolean isFramework = footer.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX);
+ binding.addFooter(new ResourceReference(stripLayoutPrefix(footer),
+ isFramework));
+ }
+
+ if (layout != null) {
+ boolean isFramework = layout.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX);
+ if (isFramework) {
+ layout = layout.substring(ANDROID_LAYOUT_RESOURCE_PREFIX.length());
+ } else if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) {
+ layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length());
+ }
+
+ binding.addItem(new DataBindingItem(layout, isFramework, 1));
+ } else if (viewObject != null) {
+ String listFqcn = ProjectCallback.getListAdapterViewFqcn(viewObject.getClass());
+ if (listFqcn != null) {
+ if (listFqcn.endsWith(EXPANDABLE_LIST_VIEW)) {
+ binding.addItem(
+ new DataBindingItem(DEFAULT_EXPANDABLE_LIST_ITEM,
+ true /* isFramework */, 1));
+ } else {
+ binding.addItem(
+ new DataBindingItem(DEFAULT_LIST_ITEM,
+ true /* isFramework */, 1));
+ }
+ }
+ } else {
+ binding.addItem(
+ new DataBindingItem(DEFAULT_LIST_ITEM,
+ true /* isFramework */, 1));
+ }
+ return binding;
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutPoint.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutPoint.java
new file mode 100644
index 000000000..818b2c4ef
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutPoint.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2010 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.ide.common.api.Point;
+
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+
+/**
+ * A {@link LayoutPoint} is a coordinate in the Android canvas (in other words,
+ * it may differ from the canvas control mouse coordinate because the canvas may
+ * be zoomed and scrolled.)
+ */
+public final class LayoutPoint {
+ /** Containing canvas which the point is relative to. */
+ private final LayoutCanvas mCanvas;
+
+ /** The X coordinate of the canvas coordinate. */
+ public final int x;
+
+ /** The Y coordinate of the canvas coordinate. */
+ public final int y;
+
+ /**
+ * Constructs a new {@link LayoutPoint} from the given event. The event
+ * must be from a {@link MouseListener} associated with the
+ * {@link LayoutCanvas} such that the {@link MouseEvent#x} and
+ * {@link MouseEvent#y} fields are relative to the canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The mouse event to construct the {@link LayoutPoint}
+ * from.
+ * @return A {@link LayoutPoint} which corresponds to the given
+ * {@link MouseEvent}.
+ */
+ public static LayoutPoint create(LayoutCanvas canvas, MouseEvent event) {
+ // The mouse event coordinates should already be relative to the canvas
+ // widget.
+ assert event.widget == canvas : event.widget;
+ return ControlPoint.create(canvas, event).toLayout();
+ }
+
+ /**
+ * Constructs a new {@link LayoutPoint} from the given event. The event
+ * must be from a {@link DragSourceListener} associated with the
+ * {@link LayoutCanvas} such that the {@link DragSourceEvent#x} and
+ * {@link DragSourceEvent#y} fields are relative to the canvas.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param event The mouse event to construct the {@link LayoutPoint}
+ * from.
+ * @return A {@link LayoutPoint} which corresponds to the given
+ * {@link DragSourceEvent}.
+ */
+ public static LayoutPoint create(LayoutCanvas canvas, DragSourceEvent event) {
+ // The drag source event coordinates should already be relative to the
+ // canvas widget.
+ return ControlPoint.create(canvas, event).toLayout();
+ }
+
+ /**
+ * Constructs a new {@link LayoutPoint} from the given x,y coordinates.
+ *
+ * @param canvas The {@link LayoutCanvas} this point is within.
+ * @param x The mouse event x coordinate relative to the canvas
+ * @param y The mouse event x coordinate relative to the canvas
+ * @return A {@link LayoutPoint} which corresponds to the given
+ * layout coordinates.
+ */
+ public static LayoutPoint create(LayoutCanvas canvas, int x, int y) {
+ return new LayoutPoint(canvas, x, y);
+ }
+
+ /**
+ * Constructs a new {@link LayoutPoint} with the given X and Y coordinates.
+ *
+ * @param canvas The canvas which contains this coordinate
+ * @param x The canvas X coordinate
+ * @param y The canvas Y coordinate
+ */
+ private LayoutPoint(LayoutCanvas canvas, int x, int y) {
+ mCanvas = canvas;
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * Returns the equivalent {@link ControlPoint} to this
+ * {@link LayoutPoint}.
+ *
+ * @return The equivalent {@link ControlPoint} to this
+ * {@link LayoutPoint}
+ */
+ public ControlPoint toControl() {
+ int cx = mCanvas.getHorizontalTransform().translate(x);
+ int cy = mCanvas.getVerticalTransform().translate(y);
+
+ return ControlPoint.create(mCanvas, cx, cy);
+ }
+
+ /**
+ * Returns this {@link LayoutPoint} as a {@link Point}, in the same coordinate space.
+ *
+ * @return a new {@link Point} in the same coordinate space
+ */
+ public Point toPoint() {
+ return new Point(x, y);
+ }
+
+ @Override
+ public String toString() {
+ return "LayoutPoint [x=" + x + ", y=" + y + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + x;
+ result = prime * result + y;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ LayoutPoint other = (LayoutPoint) obj;
+ if (x != other.x)
+ return false;
+ if (y != other.y)
+ return false;
+ return true;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutWindowCoordinator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutWindowCoordinator.java
new file mode 100644
index 000000000..56b86aa85
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutWindowCoordinator.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2012 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.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.google.common.collect.Maps;
+
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorReference;
+import org.eclipse.ui.IPartListener2;
+import org.eclipse.ui.IPartService;
+import org.eclipse.ui.IViewReference;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchPartReference;
+import org.eclipse.ui.IWorkbenchWindow;
+
+import java.util.Map;
+
+/**
+ * The {@link LayoutWindowCoordinator} keeps track of Eclipse window events (opening, closing,
+ * fronting, etc) and uses this information to manage the propertysheet and outline
+ * views such that they are always(*) showing:
+ * <ul>
+ * <li> If the Property Sheet and Outline Eclipse views are showing, it does nothing.
+ * "Showing" means "is open", not necessary "is visible", e.g. in a tabbed view
+ * there could be a different view on top.
+ * <li> If just the outline is showing, then the property sheet is shown in a sashed
+ * pane below or to the right of the outline (depending on the dominant dimension
+ * of the window).
+ * <li> TBD: If just the property sheet is showing, should the outline be showed
+ * inside that window? Not yet done.
+ * <li> If the outline is *not* showing, then the outline is instead shown
+ * <b>inside</b> the editor area, in a right-docked view! This right docked view
+ * also includes the property sheet!
+ * <li> If the property sheet is not showing (which includes not showing in the outline
+ * view as well), then it will be shown inside the editor area, along with the outline
+ * which should also be there (since if the outline was showing outside the editor
+ * area, the property sheet would have docked there).
+ * <li> When the editor is maximized, then all views are temporarily hidden. In this
+ * case, the property sheet and outline will show up inside the editor.
+ * When the editor view is un-maximized, the view state will return to what it
+ * was before.
+ * </ul>
+ * </p>
+ * There is one coordinator per workbench window, shared between all editors in that window.
+ * <p>
+ * TODO: Rename this class to AdtWindowCoordinator. It is used for more than just layout
+ * window coordination now. For example, it's also used to dispatch {@code activated()} and
+ * {@code deactivated()} events to all the XML editors, to ensure that key bindings are
+ * properly dispatched to the right editors in Eclipse 4.x.
+ */
+public class LayoutWindowCoordinator implements IPartListener2 {
+ static final String PROPERTY_SHEET_PART_ID = "org.eclipse.ui.views.PropertySheet"; //$NON-NLS-1$
+ static final String OUTLINE_PART_ID = "org.eclipse.ui.views.ContentOutline"; //$NON-NLS-1$
+ /** The workbench window */
+ private final IWorkbenchWindow mWindow;
+ /** Is the Eclipse property sheet ViewPart open? */
+ private boolean mPropertiesOpen;
+ /** Is the Eclipse outline ViewPart open? */
+ private boolean mOutlineOpen;
+ /** Is the editor maximized? */
+ private boolean mEditorMaximized;
+ /**
+ * Has the coordinator been initialized? We may have to delay initialization
+ * and perform it lazily if the workbench window does not have an active
+ * page when the coordinator is first started
+ */
+ private boolean mInitialized;
+
+ /** Map from workbench windows to each layout window coordinator instance for that window */
+ private static Map<IWorkbenchWindow, LayoutWindowCoordinator> sCoordinators =
+ Maps.newHashMapWithExpectedSize(2);
+
+ /**
+ * Returns the coordinator for the given window.
+ *
+ * @param window the associated window
+ * @param create whether to create the window if it does not already exist
+ * @return the new coordinator, never null if {@code create} is true
+ */
+ @Nullable
+ public static LayoutWindowCoordinator get(@NonNull IWorkbenchWindow window, boolean create) {
+ synchronized (LayoutWindowCoordinator.class){
+ LayoutWindowCoordinator coordinator = sCoordinators.get(window);
+ if (coordinator == null && create) {
+ coordinator = new LayoutWindowCoordinator(window);
+
+ IPartService service = window.getPartService();
+ if (service != null) {
+ // What if the editor part is *already* open? How do I deal with that?
+ service.addPartListener(coordinator);
+ }
+
+ sCoordinators.put(window, coordinator);
+ }
+
+ return coordinator;
+ }
+ }
+
+
+ /** Disposes this coordinator (when a window is closed) */
+ public void dispose() {
+ IPartService service = mWindow.getPartService();
+ if (service != null) {
+ service.removePartListener(this);
+ }
+
+ synchronized (LayoutWindowCoordinator.class){
+ sCoordinators.remove(mWindow);
+ }
+ }
+
+ /**
+ * Returns true if the main editor window is maximized
+ *
+ * @return true if the main editor window is maximized
+ */
+ public boolean isEditorMaximized() {
+ return mEditorMaximized;
+ }
+
+ private LayoutWindowCoordinator(@NonNull IWorkbenchWindow window) {
+ mWindow = window;
+
+ initialize();
+ }
+
+ private void initialize() {
+ if (mInitialized) {
+ return;
+ }
+
+ IWorkbenchPage activePage = mWindow.getActivePage();
+ if (activePage == null) {
+ return;
+ }
+
+ mInitialized = true;
+
+ // Look up current state of the properties and outline windows (in case
+ // they have already been opened before we added our part listener)
+ IViewReference ref = findPropertySheetView(activePage);
+ if (ref != null) {
+ IWorkbenchPart part = ref.getPart(false /*restore*/);
+ if (activePage.isPartVisible(part)) {
+ mPropertiesOpen = true;
+ }
+ }
+ ref = findOutlineView(activePage);
+ if (ref != null) {
+ IWorkbenchPart part = ref.getPart(false /*restore*/);
+ if (activePage.isPartVisible(part)) {
+ mOutlineOpen = true;
+ }
+ }
+ if (!syncMaximizedState(activePage)) {
+ syncActive();
+ }
+ }
+
+ static IViewReference findPropertySheetView(IWorkbenchPage activePage) {
+ return activePage.findViewReference(PROPERTY_SHEET_PART_ID);
+ }
+
+ static IViewReference findOutlineView(IWorkbenchPage activePage) {
+ return activePage.findViewReference(OUTLINE_PART_ID);
+ }
+
+ /**
+ * Checks the maximized state of the page and updates internal state if
+ * necessary.
+ * <p>
+ * This is used in Eclipse 4.x, where the {@link IPartListener2} does not
+ * fire {@link IPartListener2#partHidden(IWorkbenchPartReference)} when the
+ * editor is maximized anymore (see issue
+ * https://bugs.eclipse.org/bugs/show_bug.cgi?id=382120 for details).
+ * Instead, the layout editor listens for resize events, and upon resize it
+ * looks up the part state and calls this method to ensure that the right
+ * maximized state is known to the layout coordinator.
+ *
+ * @param page the active workbench page
+ * @return true if the state changed, false otherwise
+ */
+ public boolean syncMaximizedState(IWorkbenchPage page) {
+ boolean maximized = isPageZoomed(page);
+ if (mEditorMaximized != maximized) {
+ mEditorMaximized = maximized;
+ syncActive();
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isPageZoomed(IWorkbenchPage page) {
+ IWorkbenchPartReference reference = page.getActivePartReference();
+ if (reference != null && reference instanceof IEditorReference) {
+ int state = page.getPartState(reference);
+ boolean maximized = (state & IWorkbenchPage.STATE_MAXIMIZED) != 0;
+ return maximized;
+ }
+
+ // If the active reference isn't the editor, then the editor can't be maximized
+ return false;
+ }
+
+ /**
+ * Syncs the given editor's view state such that the property sheet and or
+ * outline are shown or hidden according to the visibility of the global
+ * outline and property sheet views.
+ * <p>
+ * This is typically done when a layout editor is fronted. For view updates
+ * when the view is already showing, the {@link LayoutWindowCoordinator}
+ * will automatically handle the current fronted window.
+ *
+ * @param editor the editor to sync
+ */
+ private void sync(@Nullable GraphicalEditorPart editor) {
+ if (editor == null) {
+ return;
+ }
+ if (mEditorMaximized) {
+ editor.showStructureViews(true /*outline*/, true /*properties*/, true /*layout*/);
+ } else if (mOutlineOpen) {
+ editor.showStructureViews(false /*outline*/, false /*properties*/, true /*layout*/);
+ editor.getCanvasControl().getOutlinePage().setShowPropertySheet(!mPropertiesOpen);
+ } else {
+ editor.showStructureViews(true /*outline*/, !mPropertiesOpen /*properties*/,
+ true /*layout*/);
+ }
+ }
+
+ private void sync(IWorkbenchPart part) {
+ if (part instanceof AndroidXmlEditor) {
+ LayoutEditorDelegate editor = LayoutEditorDelegate.fromEditor((IEditorPart) part);
+ if (editor != null) {
+ sync(editor.getGraphicalEditor());
+ }
+ }
+ }
+
+ private void syncActive() {
+ IWorkbenchPage activePage = mWindow.getActivePage();
+ if (activePage != null) {
+ IEditorPart editor = activePage.getActiveEditor();
+ sync(editor);
+ }
+ }
+
+ private void propertySheetClosed() {
+ mPropertiesOpen = false;
+ syncActive();
+ }
+
+ private void propertySheetOpened() {
+ mPropertiesOpen = true;
+ syncActive();
+ }
+
+ private void outlineClosed() {
+ mOutlineOpen = false;
+ syncActive();
+ }
+
+ private void outlineOpened() {
+ mOutlineOpen = true;
+ syncActive();
+ }
+
+ // ---- Implements IPartListener2 ----
+
+ @Override
+ public void partOpened(IWorkbenchPartReference partRef) {
+ // We ignore partOpened() and partClosed() because these methods are only
+ // called when a view is opened in the first perspective, and closed in the
+ // last perspective. The outline is typically used in multiple perspectives,
+ // so closing it in the Java perspective does *not* fire a partClosed event.
+ // There is no notification for "part closed in perspective" (see issue
+ // https://bugs.eclipse.org/bugs/show_bug.cgi?id=54559 for details).
+ // However, the workaround we can use is to listen to partVisible() and
+ // partHidden(). These will be called more often than we'd like (e.g.
+ // when the tab order causes a view to be obscured), however, we can use
+ // the workaround of looking up IWorkbenchPage.findViewReference(id) after
+ // partHidden(), which will return null if the view is closed in the current
+ // perspective. For partOpened, we simply look in partVisible() for whether
+ // our flags tracking the view state have been initialized already.
+ }
+
+ @Override
+ public void partClosed(IWorkbenchPartReference partRef) {
+ // partClosed() doesn't get called when a window is closed unless it has
+ // been closed in *all* perspectives. See partOpened() for more.
+ }
+
+ @Override
+ public void partHidden(IWorkbenchPartReference partRef) {
+ IWorkbenchPage activePage = mWindow.getActivePage();
+ if (activePage == null) {
+ return;
+ }
+ initialize();
+
+ // See if this looks like the window was closed in this workspace
+ // See partOpened() for an explanation.
+ String id = partRef.getId();
+ if (PROPERTY_SHEET_PART_ID.equals(id)) {
+ if (activePage.findViewReference(id) == null) {
+ propertySheetClosed();
+ return;
+ }
+ } else if (OUTLINE_PART_ID.equals(id)) {
+ if (activePage.findViewReference(id) == null) {
+ outlineClosed();
+ return;
+ }
+ }
+
+ // Does this look like a window getting maximized?
+ syncMaximizedState(activePage);
+ }
+
+ @Override
+ public void partVisible(IWorkbenchPartReference partRef) {
+ IWorkbenchPage activePage = mWindow.getActivePage();
+ if (activePage == null) {
+ return;
+ }
+ initialize();
+
+ String id = partRef.getId();
+ if (mEditorMaximized) {
+ // Return to their non-maximized state
+ mEditorMaximized = false;
+ syncActive();
+ }
+
+ IWorkbenchPart part = partRef.getPart(false /*restore*/);
+ sync(part);
+
+ // See partOpened() for an explanation
+ if (PROPERTY_SHEET_PART_ID.equals(id)) {
+ if (!mPropertiesOpen) {
+ propertySheetOpened();
+ assert mPropertiesOpen;
+ }
+ } else if (OUTLINE_PART_ID.equals(id)) {
+ if (!mOutlineOpen) {
+ outlineOpened();
+ assert mOutlineOpen;
+ }
+ }
+ }
+
+ @Override
+ public void partInputChanged(IWorkbenchPartReference partRef) {
+ }
+
+ @Override
+ public void partActivated(IWorkbenchPartReference partRef) {
+ IWorkbenchPart part = partRef.getPart(false);
+ if (part instanceof AndroidXmlEditor) {
+ ((AndroidXmlEditor)part).activated();
+ }
+ }
+
+ @Override
+ public void partBroughtToTop(IWorkbenchPartReference partRef) {
+ }
+
+ @Override
+ public void partDeactivated(IWorkbenchPartReference partRef) {
+ IWorkbenchPart part = partRef.getPart(false);
+ if (part instanceof AndroidXmlEditor) {
+ ((AndroidXmlEditor)part).deactivated();
+ }
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java
new file mode 100644
index 000000000..ca74493e8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2010 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.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.google.common.collect.Lists;
+
+import org.eclipse.core.resources.IMarker;
+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.w3c.dom.Node;
+
+import java.util.Collection;
+
+/**
+ * The {@link LintOverlay} paints an icon over each view that contains at least one
+ * lint error (unless the view is smaller than the icon)
+ */
+public class LintOverlay extends Overlay {
+ /** Approximate size of lint overlay icons */
+ static final int ICON_SIZE = 8;
+ /** Alpha to draw lint overlay icons with */
+ private static final int ALPHA = 192;
+
+ private final LayoutCanvas mCanvas;
+ private Image mWarningImage;
+ private Image mErrorImage;
+
+ /**
+ * Constructs a new {@link LintOverlay}
+ *
+ * @param canvas the associated canvas
+ */
+ public LintOverlay(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ }
+
+ @Override
+ public boolean isHiding() {
+ return super.isHiding() || !AdtPrefs.getPrefs().isLintOnSave();
+ }
+
+ @Override
+ public void paint(GC gc) {
+ LayoutEditorDelegate editor = mCanvas.getEditorDelegate();
+ Collection<Node> nodes = editor.getLintNodes();
+ if (nodes != null && !nodes.isEmpty()) {
+ // Copy list before iterating through it to avoid a concurrent list modification
+ // in case lint runs in the background while painting and updates this list
+ nodes = Lists.newArrayList(nodes);
+ ViewHierarchy hierarchy = mCanvas.getViewHierarchy();
+ Image icon = getWarningIcon();
+ ImageData imageData = icon.getImageData();
+ int iconWidth = imageData.width;
+ int iconHeight = imageData.height;
+ CanvasTransform mHScale = mCanvas.getHorizontalTransform();
+ CanvasTransform mVScale = mCanvas.getVerticalTransform();
+
+ // Right/bottom edges of the canvas image; don't paint overlays outside of
+ // that. (With for example RelativeLayouts with margins rendered on smaller
+ // screens than they are intended for this can happen.)
+ int maxX = mHScale.translate(0) + mHScale.getScaledImgSize();
+ int maxY = mVScale.translate(0) + mVScale.getScaledImgSize();
+
+ int oldAlpha = gc.getAlpha();
+ try {
+ gc.setAlpha(ALPHA);
+ for (Node node : nodes) {
+ CanvasViewInfo vi = hierarchy.findViewInfoFor(node);
+ if (vi != null) {
+ Rectangle bounds = vi.getAbsRect();
+ int x = mHScale.translate(bounds.x);
+ int y = mVScale.translate(bounds.y);
+ int w = mHScale.scale(bounds.width);
+ int h = mVScale.scale(bounds.height);
+ if (w < iconWidth || h < iconHeight) {
+ // Don't draw badges on tiny widgets (including those
+ // that aren't tiny but are zoomed out too far)
+ continue;
+ }
+
+ x += w - iconWidth;
+ y += h - iconHeight;
+
+ if (x > maxX || y > maxY) {
+ continue;
+ }
+
+ boolean isError = false;
+ IMarker marker = editor.getIssueForNode(vi.getUiViewNode());
+ if (marker != null) {
+ int severity = marker.getAttribute(IMarker.SEVERITY, 0);
+ isError = severity == IMarker.SEVERITY_ERROR;
+ }
+
+ icon = isError ? getErrorIcon() : getWarningIcon();
+
+ gc.drawImage(icon, x, y);
+ }
+ }
+ } finally {
+ gc.setAlpha(oldAlpha);
+ }
+ }
+ }
+
+ private Image getWarningIcon() {
+ if (mWarningImage == null) {
+ mWarningImage = IconFactory.getInstance().getIcon("warning-badge"); //$NON-NLS-1$
+ }
+
+ return mWarningImage;
+ }
+
+ private Image getErrorIcon() {
+ if (mErrorImage == null) {
+ mErrorImage = IconFactory.getInstance().getIcon("error-badge"); //$NON-NLS-1$
+ }
+
+ return mErrorImage;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltip.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltip.java
new file mode 100644
index 000000000..cedd43659
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltip.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2012 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 static com.android.SdkConstants.ATTR_ID;
+
+import com.android.ide.common.layout.BaseLayoutRule;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+import java.util.List;
+
+/** Actual tooltip showing multiple lines for various widgets that have lint errors */
+class LintTooltip extends Shell {
+ private final LayoutCanvas mCanvas;
+ private final List<UiViewElementNode> mNodes;
+
+ LintTooltip(LayoutCanvas canvas, List<UiViewElementNode> nodes) {
+ super(canvas.getDisplay(), SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL);
+ mCanvas = canvas;
+ mNodes = nodes;
+
+ createContents();
+ }
+
+ protected void createContents() {
+ Display display = getDisplay();
+ Color fg = display.getSystemColor(SWT.COLOR_INFO_FOREGROUND);
+ Color bg = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND);
+ setBackground(bg);
+ GridLayout gridLayout = new GridLayout(2, false);
+ setLayout(gridLayout);
+
+ LayoutEditorDelegate delegate = mCanvas.getEditorDelegate();
+
+ boolean first = true;
+ for (UiViewElementNode node : mNodes) {
+ IMarker marker = delegate.getIssueForNode(node);
+ if (marker != null) {
+ String message = marker.getAttribute(IMarker.MESSAGE, null);
+ if (message != null) {
+ Label icon = new Label(this, SWT.NONE);
+ icon.setForeground(fg);
+ icon.setBackground(bg);
+ icon.setImage(node.getIcon());
+
+ Label label = new Label(this, SWT.WRAP);
+ if (first) {
+ label.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1));
+ first = false;
+ }
+
+ String id = BaseLayoutRule.stripIdPrefix(node.getAttributeValue(ATTR_ID));
+ if (id.isEmpty()) {
+ if (node.getXmlNode() != null) {
+ id = node.getXmlNode().getNodeName();
+ } else {
+ id = node.getDescriptor().getUiName();
+ }
+ }
+
+ label.setText(String.format("%1$s: %2$s", id, message));
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void checkSubclass() {
+ // Disable the check that prevents subclassing of SWT components
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltipManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltipManager.java
new file mode 100644
index 000000000..f71935889
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintTooltipManager.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2012 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 static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LintOverlay.ICON_SIZE;
+
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/** Tooltip in the layout editor showing lint errors under the cursor */
+class LintTooltipManager implements Listener {
+ private final LayoutCanvas mCanvas;
+ private Shell mTip = null;
+ private List<UiViewElementNode> mShowingNodes;
+
+ /**
+ * Sets up a custom tooltip when hovering over tree items. It currently displays the error
+ * message for the lint warning associated with each node, if any (and only if the hover
+ * is over the icon portion).
+ */
+ LintTooltipManager(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ }
+
+ void register() {
+ mCanvas.addListener(SWT.Dispose, this);
+ mCanvas.addListener(SWT.KeyDown, this);
+ mCanvas.addListener(SWT.MouseMove, this);
+ mCanvas.addListener(SWT.MouseHover, this);
+ }
+
+ void unregister() {
+ if (!mCanvas.isDisposed()) {
+ mCanvas.removeListener(SWT.Dispose, this);
+ mCanvas.removeListener(SWT.KeyDown, this);
+ mCanvas.removeListener(SWT.MouseMove, this);
+ mCanvas.removeListener(SWT.MouseHover, this);
+ }
+ }
+
+ @Override
+ public void handleEvent(Event event) {
+ switch(event.type) {
+ case SWT.MouseMove:
+ // See if we're still overlapping this or *other* errors; if so, keep the
+ // tip up (or update it).
+ if (mShowingNodes != null) {
+ List<UiViewElementNode> nodes = computeNodes(event);
+ if (nodes != null && !nodes.isEmpty()) {
+ if (nodes.equals(mShowingNodes)) {
+ return;
+ } else {
+ show(nodes);
+ }
+ break;
+ }
+ }
+
+ // If not, fall through and hide the tooltip
+
+ //$FALL-THROUGH$
+ case SWT.Dispose:
+ case SWT.FocusOut:
+ case SWT.KeyDown:
+ case SWT.MouseExit:
+ case SWT.MouseDown:
+ hide();
+ break;
+ case SWT.MouseHover:
+ hide();
+ show(event);
+ break;
+ }
+ }
+
+ void hide() {
+ if (mTip != null) {
+ mTip.dispose();
+ mTip = null;
+ }
+ mShowingNodes = null;
+ }
+
+ private void show(Event event) {
+ List<UiViewElementNode> nodes = computeNodes(event);
+ if (nodes != null && !nodes.isEmpty()) {
+ show(nodes);
+ }
+ }
+
+ /** Show a tooltip listing the lint errors for the given nodes */
+ private void show(List<UiViewElementNode> nodes) {
+ hide();
+
+ if (!AdtPrefs.getPrefs().isLintOnSave()) {
+ return;
+ }
+
+ mTip = new LintTooltip(mCanvas, nodes);
+ Rectangle rect = mCanvas.getBounds();
+ Point size = mTip.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+ Point pos = mCanvas.toDisplay(rect.x, rect.y + rect.height);
+ if (size.x > rect.width) {
+ size = mTip.computeSize(rect.width, SWT.DEFAULT);
+ }
+ mTip.setBounds(pos.x, pos.y, size.x, size.y);
+
+ mShowingNodes = nodes;
+ mTip.setVisible(true);
+ }
+
+ /**
+ * Compute the list of nodes which have lint warnings near the given mouse
+ * coordinates
+ *
+ * @param event the mouse cursor event
+ * @return a list of nodes, possibly empty
+ */
+ @Nullable
+ private List<UiViewElementNode> computeNodes(Event event) {
+ LayoutPoint p = ControlPoint.create(mCanvas, event.x, event.y).toLayout();
+ LayoutEditorDelegate delegate = mCanvas.getEditorDelegate();
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ CanvasTransform mHScale = mCanvas.getHorizontalTransform();
+ CanvasTransform mVScale = mCanvas.getVerticalTransform();
+
+ int layoutIconSize = mHScale.inverseScale(ICON_SIZE);
+ int slop = mVScale.inverseScale(10); // extra space around icon where tip triggers
+
+ Collection<Node> xmlNodes = delegate.getLintNodes();
+ if (xmlNodes == null) {
+ return null;
+ }
+ List<UiViewElementNode> nodes = new ArrayList<UiViewElementNode>();
+ for (Node xmlNode : xmlNodes) {
+ CanvasViewInfo v = viewHierarchy.findViewInfoFor(xmlNode);
+ if (v != null) {
+ Rectangle b = v.getAbsRect();
+ int x2 = b.x + b.width;
+ int y2 = b.y + b.height;
+ if (p.x < x2 - layoutIconSize - slop
+ || p.x > x2 + slop
+ || p.y < y2 - layoutIconSize - slop
+ || p.y > y2 + slop) {
+ continue;
+ }
+
+ nodes.add(v.getUiViewNode());
+ }
+ }
+
+ return nodes;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java
new file mode 100644
index 000000000..4577f8d12
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2011 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 static com.android.SdkConstants.ANDROID_LAYOUT_RESOURCE_PREFIX;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata.KEY_LV_FOOTER;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata.KEY_LV_HEADER;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata.KEY_LV_ITEM;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.resources.CyclicDependencyValidator;
+import com.android.ide.eclipse.adt.internal.ui.ResourceChooser;
+import com.android.resources.ResourceType;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.widgets.Menu;
+import org.w3c.dom.Node;
+
+/**
+ * "Preview List Content" context menu which lists available data types and layouts
+ * the user can choose to view the ListView as.
+ */
+public class ListViewTypeMenu extends SubmenuAction {
+ /** Associated canvas */
+ private final LayoutCanvas mCanvas;
+ /** When true, this menu is for a grid rather than a simple list */
+ private boolean mGrid;
+ /** When true, this menu is for a spinner rather than a simple list */
+ private boolean mSpinner;
+
+ /**
+ * Creates a "Preview List Content" menu
+ *
+ * @param canvas associated canvas
+ * @param isGrid whether the menu is for a grid rather than a list
+ * @param isSpinner whether the menu is for a spinner rather than a list
+ */
+ public ListViewTypeMenu(LayoutCanvas canvas, boolean isGrid, boolean isSpinner) {
+ super(isGrid ? "Preview Grid Content" : isSpinner ? "Preview Spinner Layout"
+ : "Preview List Content");
+ mCanvas = canvas;
+ mGrid = isGrid;
+ mSpinner = isSpinner;
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ if (graphicalEditor.renderingSupports(Capability.ADAPTER_BINDING)) {
+ IAction action = new PickLayoutAction("Choose Layout...", KEY_LV_ITEM);
+ new ActionContributionItem(action).fill(menu, -1);
+ new Separator().fill(menu, -1);
+
+ String selected = getSelectedLayout();
+ if (selected != null) {
+ if (selected.startsWith(ANDROID_LAYOUT_RESOURCE_PREFIX)) {
+ selected = selected.substring(ANDROID_LAYOUT_RESOURCE_PREFIX.length());
+ }
+ }
+
+ if (mSpinner) {
+ action = new SetListTypeAction("Spinner Item",
+ "simple_spinner_item", selected); //$NON-NLS-1$
+ new ActionContributionItem(action).fill(menu, -1);
+ action = new SetListTypeAction("Spinner Dropdown Item",
+ "simple_spinner_dropdown_item", selected); //$NON-NLS-1$
+ new ActionContributionItem(action).fill(menu, -1);
+ return;
+ }
+
+ action = new SetListTypeAction("Simple List Item",
+ "simple_list_item_1", selected); //$NON-NLS-1$
+ new ActionContributionItem(action).fill(menu, -1);
+ action = new SetListTypeAction("Simple 2-Line List Item",
+ "simple_list_item_2", //$NON-NLS-1$
+ selected);
+ new ActionContributionItem(action).fill(menu, -1);
+ action = new SetListTypeAction("Checked List Item",
+ "simple_list_item_checked", //$NON-NLS-1$
+ selected);
+ new ActionContributionItem(action).fill(menu, -1);
+ action = new SetListTypeAction("Single Choice List Item",
+ "simple_list_item_single_choice", //$NON-NLS-1$
+ selected);
+ new ActionContributionItem(action).fill(menu, -1);
+ action = new SetListTypeAction("Multiple Choice List Item",
+ "simple_list_item_multiple_choice", //$NON-NLS-1$
+ selected);
+ if (!mGrid) {
+ new Separator().fill(menu, -1);
+ action = new SetListTypeAction("Simple Expandable List Item",
+ "simple_expandable_list_item_1", selected); //$NON-NLS-1$
+ new ActionContributionItem(action).fill(menu, -1);
+ action = new SetListTypeAction("Simple 2-Line Expandable List Item",
+ "simple_expandable_list_item_2", //$NON-NLS-1$
+ selected);
+ new ActionContributionItem(action).fill(menu, -1);
+
+ new Separator().fill(menu, -1);
+ action = new PickLayoutAction("Choose Header...", KEY_LV_HEADER);
+ new ActionContributionItem(action).fill(menu, -1);
+ action = new PickLayoutAction("Choose Footer...", KEY_LV_FOOTER);
+ new ActionContributionItem(action).fill(menu, -1);
+ }
+ } else {
+ // Should we just hide the menu item instead?
+ addDisabledMessageItem(
+ "Not supported for this SDK version; try changing the Render Target");
+ }
+ }
+
+ private class SetListTypeAction extends Action {
+ private final String mLayout;
+
+ public SetListTypeAction(String title, String layout, String selected) {
+ super(title, IAction.AS_RADIO_BUTTON);
+ mLayout = layout;
+
+ if (layout.equals(selected)) {
+ setChecked(true);
+ }
+ }
+
+ @Override
+ public void run() {
+ if (isChecked()) {
+ setNewType(KEY_LV_ITEM, ANDROID_LAYOUT_RESOURCE_PREFIX + mLayout);
+ }
+ }
+ }
+
+ /**
+ * Action which brings up a resource chooser to choose an arbitrary layout as the
+ * layout to be previewed in the list.
+ */
+ private class PickLayoutAction extends Action {
+ private final String mType;
+
+ public PickLayoutAction(String title, String type) {
+ super(title, IAction.AS_PUSH_BUTTON);
+ mType = type;
+ }
+
+ @Override
+ public void run() {
+ LayoutEditorDelegate delegate = mCanvas.getEditorDelegate();
+ IFile file = delegate.getEditor().getInputFile();
+ GraphicalEditorPart editor = delegate.getGraphicalEditor();
+ ResourceChooser dlg = ResourceChooser.create(editor, ResourceType.LAYOUT)
+ .setInputValidator(CyclicDependencyValidator.create(file))
+ .setInitialSize(85, 10)
+ .setCurrentResource(getSelectedLayout());
+ int result = dlg.open();
+ if (result == ResourceChooser.CLEAR_RETURN_CODE) {
+ setNewType(mType, null);
+ } else if (result == Window.OK) {
+ String newType = dlg.getCurrentResource();
+ setNewType(mType, newType);
+ }
+ }
+ }
+
+ @Nullable
+ private String getSelectedLayout() {
+ String layout = null;
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+ for (SelectionItem item : selectionManager.getSelections()) {
+ UiViewElementNode node = item.getViewInfo().getUiViewNode();
+ if (node != null) {
+ Node xmlNode = node.getXmlNode();
+ layout = LayoutMetadata.getProperty(xmlNode, KEY_LV_ITEM);
+ if (layout != null) {
+ return layout;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private void setNewType(@NonNull String type, @Nullable String layout) {
+ LayoutEditorDelegate delegate = mCanvas.getEditorDelegate();
+ GraphicalEditorPart graphicalEditor = delegate.getGraphicalEditor();
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+
+ for (SelectionItem item : selectionManager.getSnapshot()) {
+ UiViewElementNode node = item.getViewInfo().getUiViewNode();
+ if (node != null) {
+ Node xmlNode = node.getXmlNode();
+ LayoutMetadata.setProperty(delegate.getEditor(), xmlNode, type, layout);
+ }
+ }
+
+ // Refresh
+ graphicalEditor.recomputeLayout();
+ mCanvas.redraw();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MarqueeGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MarqueeGesture.java
new file mode 100644
index 000000000..4cfd4fe3d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MarqueeGesture.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2010 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 org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A {@link MarqueeGesture} is a gesture for swiping out a selection rectangle.
+ * With a modifier key, items that intersect the rectangle can be toggled
+ * instead of added to the new selection set.
+ */
+public class MarqueeGesture extends Gesture {
+ /** The {@link Overlay} drawn for the marquee. */
+ private MarqueeOverlay mOverlay;
+
+ /** The canvas associated with this gesture. */
+ private LayoutCanvas mCanvas;
+
+ /** A copy of the initial selection, when we're toggling the marquee. */
+ private Collection<CanvasViewInfo> mInitialSelection;
+
+ /**
+ * Creates a new marquee selection (selection swiping).
+ *
+ * @param canvas The canvas where selection is performed.
+ * @param toggle If true, toggle the membership of contained elements
+ * instead of adding it.
+ */
+ public MarqueeGesture(LayoutCanvas canvas, boolean toggle) {
+ mCanvas = canvas;
+
+ if (toggle) {
+ List<SelectionItem> selection = canvas.getSelectionManager().getSelections();
+ mInitialSelection = new ArrayList<CanvasViewInfo>(selection.size());
+ for (SelectionItem item : selection) {
+ mInitialSelection.add(item.getViewInfo());
+ }
+ } else {
+ mInitialSelection = Collections.emptySet();
+ }
+ }
+
+ @Override
+ public void update(ControlPoint pos) {
+ if (mOverlay == null) {
+ return;
+ }
+
+ int x = Math.min(pos.x, mStart.x);
+ int y = Math.min(pos.y, mStart.y);
+ int w = Math.abs(pos.x - mStart.x);
+ int h = Math.abs(pos.y - mStart.y);
+
+ mOverlay.updateSize(x, y, w, h);
+
+ // Compute selection overlaps
+ LayoutPoint topLeft = ControlPoint.create(mCanvas, x, y).toLayout();
+ LayoutPoint bottomRight = ControlPoint.create(mCanvas, x + w, y + h).toLayout();
+ mCanvas.getSelectionManager().selectWithin(topLeft, bottomRight, mInitialSelection);
+ }
+
+ @Override
+ public List<Overlay> createOverlays() {
+ mOverlay = new MarqueeOverlay();
+ return Collections.<Overlay> singletonList(mOverlay);
+ }
+
+ /**
+ * An {@link Overlay} for the {@link MarqueeGesture}; paints a selection
+ * overlay rectangle matching the mouse coordinate delta between gesture
+ * start and the current position.
+ */
+ private static class MarqueeOverlay extends Overlay {
+ /** Rectangle border color. */
+ private Color mStroke;
+
+ /** Rectangle fill color. */
+ private Color mFill;
+
+ /** Current rectangle coordinates (in terms of control coordinates). */
+ private Rectangle mRectangle = new Rectangle(0, 0, 0, 0);
+
+ /** Alpha value of the fill. */
+ private int mFillAlpha;
+
+ /** Alpha value of the border. */
+ private int mStrokeAlpha;
+
+ /** Constructs a new {@link MarqueeOverlay}. */
+ public MarqueeOverlay() {
+ }
+
+ /**
+ * Updates the size of the marquee rectangle.
+ *
+ * @param x The top left corner of the rectangle, x coordinate.
+ * @param y The top left corner of the rectangle, y coordinate.
+ * @param w Rectangle width.
+ * @param h Rectangle height.
+ */
+ public void updateSize(int x, int y, int w, int h) {
+ mRectangle.x = x;
+ mRectangle.y = y;
+ mRectangle.width = w;
+ mRectangle.height = h;
+ }
+
+ @Override
+ public void create(Device device) {
+ // TODO: Integrate DrawingStyles with this?
+ mStroke = new Color(device, 255, 255, 255);
+ mFill = new Color(device, 128, 128, 128);
+ mFillAlpha = 64;
+ mStrokeAlpha = 255;
+ }
+
+ @Override
+ public void dispose() {
+ mStroke.dispose();
+ mFill.dispose();
+ }
+
+ @Override
+ public void paint(GC gc) {
+ if (mRectangle.width > 0 && mRectangle.height > 0) {
+ gc.setLineStyle(SWT.LINE_SOLID);
+ gc.setLineWidth(1);
+ gc.setForeground(mStroke);
+ gc.setBackground(mFill);
+ gc.setAlpha(mStrokeAlpha);
+ gc.drawRectangle(mRectangle);
+ gc.setAlpha(mFillAlpha);
+ gc.fillRectangle(mRectangle);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java
new file mode 100644
index 000000000..7cf3a647a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java
@@ -0,0 +1,852 @@
+/*
+ * Copyright (C) 2010 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.ide.common.api.DropFeedback;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.InsertType;
+import com.android.ide.common.api.Point;
+import com.android.ide.common.api.Rect;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode.NodeCreationListener;
+
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.widgets.Display;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * The Move gesture provides the operation for moving widgets around in the canvas.
+ */
+public class MoveGesture extends DropGesture {
+ /** The associated {@link LayoutCanvas}. */
+ private LayoutCanvas mCanvas;
+
+ /** Overlay which paints the drag &amp; drop feedback. */
+ private MoveOverlay mOverlay;
+
+ private static final boolean DEBUG = false;
+
+ /**
+ * The top view right under the drag'n'drop cursor.
+ * This can only be null during a drag'n'drop when there is no view under the cursor
+ * or after the state was all cleared.
+ */
+ private CanvasViewInfo mCurrentView;
+
+ /**
+ * The elements currently being dragged. This will always be non-null for a valid
+ * drag'n'drop that happens within the same instance of Eclipse.
+ * <p/>
+ * In the event that the drag and drop happens between different instances of Eclipse
+ * this will remain null.
+ */
+ private SimpleElement[] mCurrentDragElements;
+
+ /**
+ * The first view under the cursor that responded to onDropEnter is called the "target view".
+ * It can differ from mCurrentView, typically because a terminal View doesn't
+ * accept drag'n'drop so its parent layout became the target drag'n'drop receiver.
+ * <p/>
+ * The target node is the proxy node associated with the target view.
+ * This can be null if no view under the cursor accepted the drag'n'drop or if the node
+ * factory couldn't create a proxy for it.
+ */
+ private NodeProxy mTargetNode;
+
+ /**
+ * The latest drop feedback returned by IViewRule.onDropEnter/Move.
+ */
+ private DropFeedback mFeedback;
+
+ /**
+ * {@link #dragLeave(DropTargetEvent)} is unfortunately called right before data is
+ * about to be dropped (between the last {@link #dragOver(DropTargetEvent)} and the
+ * next {@link #dropAccept(DropTargetEvent)}). That means we can't just
+ * trash the current DropFeedback from the current view rule in dragLeave().
+ * Instead we preserve it in mLeaveTargetNode and mLeaveFeedback in case a dropAccept
+ * happens next.
+ */
+ private NodeProxy mLeaveTargetNode;
+
+ /**
+ * @see #mLeaveTargetNode
+ */
+ private DropFeedback mLeaveFeedback;
+
+ /**
+ * @see #mLeaveTargetNode
+ */
+ private CanvasViewInfo mLeaveView;
+
+ /** Singleton used to keep track of drag selection in the same Eclipse instance. */
+ private final GlobalCanvasDragInfo mGlobalDragInfo;
+
+ /**
+ * Constructs a new {@link MoveGesture}, tied to the given canvas.
+ *
+ * @param canvas The canvas to associate the {@link MoveGesture} with.
+ */
+ public MoveGesture(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ mGlobalDragInfo = GlobalCanvasDragInfo.getInstance();
+ }
+
+ @Override
+ public List<Overlay> createOverlays() {
+ mOverlay = new MoveOverlay();
+ return Collections.<Overlay> singletonList(mOverlay);
+ }
+
+ @Override
+ public void begin(ControlPoint pos, int startMask) {
+ super.begin(pos, startMask);
+
+ // Hide selection overlays during a move drag
+ mCanvas.getSelectionOverlay().setHidden(true);
+ }
+
+ @Override
+ public void end(ControlPoint pos, boolean canceled) {
+ super.end(pos, canceled);
+
+ mCanvas.getSelectionOverlay().setHidden(false);
+
+ // Ensure that the outline is back to showing the current selection, since during
+ // a drag gesture we temporarily set it to show the current target node instead.
+ mCanvas.getSelectionManager().syncOutlineSelection();
+ }
+
+ /* TODO: Pass modifier mask to drag rules as well! This doesn't work yet since
+ the drag &amp; drop code seems to steal keyboard events.
+ @Override
+ public boolean keyPressed(KeyEvent event) {
+ update(mCanvas.getGestureManager().getCurrentControlPoint());
+ mCanvas.redraw();
+ return true;
+ }
+
+ @Override
+ public boolean keyReleased(KeyEvent event) {
+ update(mCanvas.getGestureManager().getCurrentControlPoint());
+ mCanvas.redraw();
+ return true;
+ }
+ */
+
+ /*
+ * The cursor has entered the drop target boundaries.
+ * {@inheritDoc}
+ */
+ @Override
+ public void dragEnter(DropTargetEvent event) {
+ if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag enter", event);
+
+ // Make sure we don't have any residual data from an earlier operation.
+ clearDropInfo();
+ mLeaveTargetNode = null;
+ mLeaveFeedback = null;
+ mLeaveView = null;
+
+ // Get the dragged elements.
+ //
+ // The current transfered type can be extracted from the event.
+ // As described in dragOver(), this works basically works on Windows but
+ // not on Linux or Mac, in which case we can't get the type until we
+ // receive dropAccept/drop().
+ // For consistency we try to use the GlobalCanvasDragInfo instance first,
+ // and if it fails we use the event transfer type as a backup (but as said
+ // before it will most likely work only on Windows.)
+ // In any case this can be null even for a valid transfer.
+
+ mCurrentDragElements = mGlobalDragInfo.getCurrentElements();
+
+ if (mCurrentDragElements == null) {
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+ if (sxt.isSupportedType(event.currentDataType)) {
+ mCurrentDragElements = (SimpleElement[]) sxt.nativeToJava(event.currentDataType);
+ }
+ }
+
+ // if there is no data to transfer, invalidate the drag'n'drop.
+ // The assumption is that the transfer should have at least one element with a
+ // a non-null non-empty FQCN. Everything else is optional.
+ if (mCurrentDragElements == null ||
+ mCurrentDragElements.length == 0 ||
+ mCurrentDragElements[0] == null ||
+ mCurrentDragElements[0].getFqcn() == null ||
+ mCurrentDragElements[0].getFqcn().length() == 0) {
+ event.detail = DND.DROP_NONE;
+ }
+
+ dragOperationChanged(event);
+ }
+
+ /*
+ * The operation being performed has changed (e.g. modifier key).
+ * {@inheritDoc}
+ */
+ @Override
+ public void dragOperationChanged(DropTargetEvent event) {
+ if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag changed", event);
+
+ checkDataType(event);
+ recomputeDragType(event);
+ }
+
+ private void recomputeDragType(DropTargetEvent event) {
+ if (event.detail == DND.DROP_DEFAULT) {
+ // Default means we can now choose the default operation, either copy or move.
+ // If the drag comes from the same canvas we default to move, otherwise we
+ // default to copy.
+
+ if (mGlobalDragInfo.getSourceCanvas() == mCanvas &&
+ (event.operations & DND.DROP_MOVE) != 0) {
+ event.detail = DND.DROP_MOVE;
+ } else if ((event.operations & DND.DROP_COPY) != 0) {
+ event.detail = DND.DROP_COPY;
+ }
+ }
+
+ // We don't support other types than copy and move
+ if (event.detail != DND.DROP_COPY && event.detail != DND.DROP_MOVE) {
+ event.detail = DND.DROP_NONE;
+ }
+ }
+
+ /*
+ * The cursor has left the drop target boundaries OR data is about to be dropped.
+ * {@inheritDoc}
+ */
+ @Override
+ public void dragLeave(DropTargetEvent event) {
+ if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag leave");
+
+ // dragLeave is unfortunately called right before data is about to be dropped
+ // (between the last dropMove and the next dropAccept). That means we can't just
+ // trash the current DropFeedback from the current view rule, we need to preserve
+ // it in case a dropAccept happens next.
+ // See the corresponding kludge in dropAccept().
+ mLeaveTargetNode = mTargetNode;
+ mLeaveFeedback = mFeedback;
+ mLeaveView = mCurrentView;
+
+ clearDropInfo();
+ }
+
+ /*
+ * The cursor is moving over the drop target.
+ * {@inheritDoc}
+ */
+ @Override
+ public void dragOver(DropTargetEvent event) {
+ processDropEvent(event);
+ }
+
+ /*
+ * The drop is about to be performed.
+ * The drop target is given a last chance to change the nature of the drop.
+ * {@inheritDoc}
+ */
+ @Override
+ public void dropAccept(DropTargetEvent event) {
+ if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drop accept");
+
+ checkDataType(event);
+
+ // If we have a valid target node and it matches the one we saved in
+ // dragLeave then we restore the DropFeedback that we saved in dragLeave.
+ if (mLeaveTargetNode != null) {
+ mTargetNode = mLeaveTargetNode;
+ mFeedback = mLeaveFeedback;
+ mCurrentView = mLeaveView;
+ }
+
+ if (mFeedback != null && mFeedback.invalidTarget) {
+ // The script said we can't drop here.
+ event.detail = DND.DROP_NONE;
+ }
+
+ if (mLeaveTargetNode == null || event.detail == DND.DROP_NONE) {
+ clearDropInfo();
+ }
+
+ mLeaveTargetNode = null;
+ mLeaveFeedback = null;
+ mLeaveView = null;
+ }
+
+ /*
+ * The data is being dropped.
+ * {@inheritDoc}
+ */
+ @Override
+ public void drop(final DropTargetEvent event) {
+ if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "dropped");
+
+ SimpleElement[] elements = null;
+
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+
+ if (sxt.isSupportedType(event.currentDataType)) {
+ if (event.data instanceof SimpleElement[]) {
+ elements = (SimpleElement[]) event.data;
+ }
+ }
+
+ if (elements == null || elements.length < 1) {
+ if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drop missing drop data");
+ return;
+ }
+
+ if (mCurrentDragElements != null && Arrays.equals(elements, mCurrentDragElements)) {
+ elements = mCurrentDragElements;
+ }
+
+ if (mTargetNode == null) {
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ if (viewHierarchy.isValid() && viewHierarchy.isEmpty()) {
+ // There is no target node because the drop happens on an empty document.
+ // Attempt to create a root node accordingly.
+ createDocumentRoot(elements);
+ } else {
+ if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "dropped on null targetNode");
+ }
+ return;
+ }
+
+ updateDropFeedback(mFeedback, event);
+
+ final SimpleElement[] elementsFinal = elements;
+ final LayoutPoint canvasPoint = getDropLocation(event).toLayout();
+ String label = computeUndoLabel(mTargetNode, elements, event.detail);
+
+ // Create node listener which (during the drop) listens for node additions
+ // and stores the list of added node such that they can be selected afterwards.
+ final List<UiElementNode> added = new ArrayList<UiElementNode>();
+ // List of "index within parent" for each node
+ final List<Integer> indices = new ArrayList<Integer>();
+ NodeCreationListener listener = new NodeCreationListener() {
+ @Override
+ public void nodeCreated(UiElementNode parent, UiElementNode child, int index) {
+ if (parent == mTargetNode.getNode()) {
+ added.add(child);
+
+ // Adjust existing indices
+ for (int i = 0, n = indices.size(); i < n; i++) {
+ int idx = indices.get(i);
+ if (idx >= index) {
+ indices.set(i, idx + 1);
+ }
+ }
+
+ indices.add(index);
+ }
+ }
+
+ @Override
+ public void nodeDeleted(UiElementNode parent, UiElementNode child, int previousIndex) {
+ if (parent == mTargetNode.getNode()) {
+ // Adjust existing indices
+ for (int i = 0, n = indices.size(); i < n; i++) {
+ int idx = indices.get(i);
+ if (idx >= previousIndex) {
+ indices.set(i, idx - 1);
+ }
+ }
+
+ // Make sure we aren't removing the same nodes that are being added
+ // No, that can happen when canceling out of a drop handler such as
+ // when dropping an included layout, then canceling out of the
+ // resource chooser.
+ //assert !added.contains(child);
+ }
+ }
+ };
+
+ try {
+ UiElementNode.addNodeCreationListener(listener);
+ mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(label, new Runnable() {
+ @Override
+ public void run() {
+ InsertType insertType = getInsertType(event, mTargetNode);
+ mCanvas.getRulesEngine().callOnDropped(mTargetNode,
+ elementsFinal,
+ mFeedback,
+ new Point(canvasPoint.x, canvasPoint.y),
+ insertType);
+ mTargetNode.applyPendingChanges();
+ // Clean up drag if applicable
+ if (event.detail == DND.DROP_MOVE) {
+ GlobalCanvasDragInfo.getInstance().removeSource();
+ }
+ mTargetNode.applyPendingChanges();
+ }
+ });
+ } finally {
+ UiElementNode.removeNodeCreationListener(listener);
+ }
+
+ final List<INode> nodes = new ArrayList<INode>();
+ NodeFactory nodeFactory = mCanvas.getNodeFactory();
+ for (UiElementNode uiNode : added) {
+ if (uiNode instanceof UiViewElementNode) {
+ NodeProxy node = nodeFactory.create((UiViewElementNode) uiNode);
+ if (node != null) {
+ nodes.add(node);
+ }
+ }
+ }
+
+ // Select the newly dropped nodes:
+ // Find out which nodes were added, and look up their corresponding
+ // CanvasViewInfos.
+ final SelectionManager selectionManager = mCanvas.getSelectionManager();
+ // Don't use the indices to search for corresponding nodes yet, since a
+ // render may not have happened yet and we'd rather use an up to date
+ // view hierarchy than indices to look up the right view infos.
+ if (!selectionManager.selectDropped(nodes, null /* indices */)) {
+ // In some scenarios we can't find the actual view infos yet; this
+ // seems to happen when you drag from one canvas to another (see the
+ // related comment next to the setFocus() call below). In that case
+ // defer selection briefly until the view hierarchy etc is up to
+ // date.
+ Display.getDefault().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ selectionManager.selectDropped(nodes, indices);
+ }
+ });
+ }
+
+ clearDropInfo();
+ mCanvas.redraw();
+ // Request focus: This is *necessary* when you are dragging from one canvas editor
+ // to another, because without it, the redraw does not seem to be processed (the change
+ // is invisible until you click on the target canvas to give it focus).
+ mCanvas.setFocus();
+ }
+
+ /**
+ * Returns the right {@link InsertType} to use for the given drop target event and the
+ * given target node
+ *
+ * @param event the drop target event
+ * @param mTargetNode the node targeted by the drop
+ * @return the {link InsertType} to use for the drop
+ */
+ public static InsertType getInsertType(DropTargetEvent event, NodeProxy mTargetNode) {
+ GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance();
+ if (event.detail == DND.DROP_MOVE) {
+ SelectionItem[] selection = dragInfo.getCurrentSelection();
+ if (selection != null) {
+ for (SelectionItem item : selection) {
+ if (item.getNode() != null
+ && item.getNode().getParent() == mTargetNode) {
+ return InsertType.MOVE_WITHIN;
+ }
+ }
+ }
+
+ return InsertType.MOVE_INTO;
+ } else if (dragInfo.getSourceCanvas() != null) {
+ return InsertType.PASTE;
+ } else {
+ return InsertType.CREATE;
+ }
+ }
+
+ /**
+ * Computes a suitable Undo label to use for a drop operation, such as
+ * "Drop Button in LinearLayout" and "Move Widgets in RelativeLayout".
+ *
+ * @param targetNode The target of the drop
+ * @param elements The dragged widgets
+ * @param detail The DnD mode, as used in {@link DropTargetEvent#detail}.
+ * @return A string suitable as an undo-label for the drop event
+ */
+ public static String computeUndoLabel(NodeProxy targetNode,
+ SimpleElement[] elements, int detail) {
+ // Decide whether it's a move or a copy; we'll label moves specifically
+ // as a move and consider everything else a "Drop"
+ String verb = (detail == DND.DROP_MOVE) ? "Move" : "Drop";
+
+ // Get the type of widget being dropped/moved, IF there is only one. If
+ // there is more than one, just reference it as "Widgets".
+ String object;
+ if (elements != null && elements.length == 1) {
+ object = getSimpleName(elements[0].getFqcn());
+ } else {
+ object = "Widgets";
+ }
+
+ String where = getSimpleName(targetNode.getFqcn());
+
+ // When we localize this: $1 is the verb (Move or Drop), $2 is the
+ // object (such as "Button"), and $3 is the place we are doing it (such
+ // as "LinearLayout").
+ return String.format("%1$s %2$s in %3$s", verb, object, where);
+ }
+
+ /**
+ * Returns simple name (basename, following last dot) of a fully qualified
+ * class name.
+ *
+ * @param fqcn The fqcn to reduce
+ * @return The base name of the fqcn
+ */
+ public static String getSimpleName(String fqcn) {
+ // Note that the following works even when there is no dot, since
+ // lastIndexOf will return -1 so we get fcqn.substring(-1+1) =
+ // fcqn.substring(0) = fqcn
+ return fqcn.substring(fqcn.lastIndexOf('.') + 1);
+ }
+
+ /**
+ * Updates the {@link DropFeedback#isCopy} and {@link DropFeedback#sameCanvas} fields
+ * of the given {@link DropFeedback}. This is generally called right before invoking
+ * one of the callOnXyz methods of GRE to refresh the fields.
+ *
+ * @param df The current {@link DropFeedback}.
+ * @param event An optional event to determine if the current operation is copy or move.
+ */
+ private void updateDropFeedback(DropFeedback df, DropTargetEvent event) {
+ if (event != null) {
+ df.isCopy = event.detail == DND.DROP_COPY;
+ }
+ df.sameCanvas = mCanvas == mGlobalDragInfo.getSourceCanvas();
+ df.invalidTarget = false;
+ df.dipScale = mCanvas.getEditorDelegate().getGraphicalEditor().getDipScale();
+ df.modifierMask = mCanvas.getGestureManager().getRuleModifierMask();
+
+ // Set the drag bounds, after converting it from control coordinates to
+ // layout coordinates
+ GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance();
+ Rect dragBounds = null;
+ Rect controlDragBounds = dragInfo.getDragBounds();
+ if (controlDragBounds != null) {
+ CanvasTransform ht = mCanvas.getHorizontalTransform();
+ CanvasTransform vt = mCanvas.getVerticalTransform();
+ double horizScale = ht.getScale();
+ double verticalScale = vt.getScale();
+ int x = (int) (controlDragBounds.x / horizScale);
+ int y = (int) (controlDragBounds.y / verticalScale);
+ int w = (int) (controlDragBounds.w / horizScale);
+ int h = (int) (controlDragBounds.h / verticalScale);
+ dragBounds = new Rect(x, y, w, h);
+ }
+ int baseline = dragInfo.getDragBaseline();
+ if (baseline != -1) {
+ df.dragBaseline = baseline;
+ }
+ df.dragBounds = dragBounds;
+ }
+
+ /**
+ * Verifies that event.currentDataType is of type {@link SimpleXmlTransfer}.
+ * If not, try to find a valid data type.
+ * Otherwise set the drop to {@link DND#DROP_NONE} to cancel it.
+ *
+ * @return True if the data type is accepted.
+ */
+ private static boolean checkDataType(DropTargetEvent event) {
+
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+
+ TransferData current = event.currentDataType;
+
+ if (sxt.isSupportedType(current)) {
+ return true;
+ }
+
+ // We only support SimpleXmlTransfer and the current data type is not right.
+ // Let's see if we can find another one.
+
+ for (TransferData td : event.dataTypes) {
+ if (td != current && sxt.isSupportedType(td)) {
+ // We like this type better.
+ event.currentDataType = td;
+ return true;
+ }
+ }
+
+ // We failed to find any good transfer type.
+ event.detail = DND.DROP_NONE;
+ return false;
+ }
+
+ /**
+ * Returns the mouse location of the drop target event.
+ *
+ * @param event the drop target event
+ * @return a {@link ControlPoint} location corresponding to the top left corner
+ */
+ private ControlPoint getDropLocation(DropTargetEvent event) {
+ return ControlPoint.create(mCanvas, event);
+ }
+
+ /**
+ * Called on both dragEnter and dragMove.
+ * Generates the onDropEnter/Move/Leave events depending on the currently
+ * selected target node.
+ */
+ private void processDropEvent(DropTargetEvent event) {
+ if (!mCanvas.getViewHierarchy().isValid()) {
+ // We don't allow drop on an invalid layout, even if we have some obsolete
+ // layout info for it.
+ event.detail = DND.DROP_NONE;
+ clearDropInfo();
+ return;
+ }
+
+ LayoutPoint p = getDropLocation(event).toLayout();
+
+ // Is the mouse currently captured by a DropFeedback.captureArea?
+ boolean isCaptured = false;
+ if (mFeedback != null) {
+ Rect r = mFeedback.captureArea;
+ isCaptured = r != null && r.contains(p.x, p.y);
+ }
+
+ // We can't switch views/nodes when the mouse is captured
+ CanvasViewInfo vi;
+ if (isCaptured) {
+ vi = mCurrentView;
+ } else {
+ vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+
+ // When dragging into the canvas, if you are not over any other view, target
+ // the root element (since it may not "fill" the screen, e.g. if you have a linear
+ // layout but have layout_height wrap_content, then the layout will only extend
+ // to cover the children in the layout, not the whole visible screen area, which
+ // may be surprising
+ if (vi == null) {
+ vi = mCanvas.getViewHierarchy().getRoot();
+ }
+ }
+
+ boolean isMove = true;
+ boolean needRedraw = false;
+
+ if (vi != mCurrentView) {
+ // Current view has changed. Does that also change the target node?
+ // Note that either mCurrentView or vi can be null.
+
+ if (vi == null) {
+ // vi is null but mCurrentView is not, no view is a target anymore
+ // We don't need onDropMove in this case
+ isMove = false;
+ needRedraw = true;
+ event.detail = DND.DROP_NONE;
+ clearDropInfo(); // this will call callDropLeave.
+
+ } else {
+ // vi is a new current view.
+ // Query GRE for onDropEnter on the ViewInfo hierarchy, starting from the child
+ // towards its parent, till we find one that returns a non-null drop feedback.
+
+ DropFeedback df = null;
+ NodeProxy targetNode = null;
+
+ for (CanvasViewInfo targetVi = vi;
+ targetVi != null && df == null;
+ targetVi = targetVi.getParent()) {
+ targetNode = mCanvas.getNodeFactory().create(targetVi);
+ df = mCanvas.getRulesEngine().callOnDropEnter(targetNode,
+ targetVi.getViewObject(), mCurrentDragElements);
+
+ if (df != null) {
+ // We should also dispatch an onDropMove() call to the initial enter
+ // position, such that the view is notified of the position where
+ // we are within the node immediately (before we for example attempt
+ // to draw feedback). This is necessary since most views perform the
+ // guideline computations in onDropMove (since only onDropMove is handed
+ // the -position- of the mouse), and we want this computation to happen
+ // before we ask the view to draw its feedback.
+ updateDropFeedback(df, event);
+ df = mCanvas.getRulesEngine().callOnDropMove(targetNode,
+ mCurrentDragElements, df, new Point(p.x, p.y));
+ }
+
+ if (df != null &&
+ event.detail == DND.DROP_MOVE &&
+ mCanvas == mGlobalDragInfo.getSourceCanvas()) {
+ // You can't move an object into itself in the same canvas.
+ // E.g. case of moving a layout and the node under the mouse is the
+ // layout itself: a copy would be ok but not a move operation of the
+ // layout into himself.
+
+ SelectionItem[] selection = mGlobalDragInfo.getCurrentSelection();
+ if (selection != null) {
+ for (SelectionItem cs : selection) {
+ if (cs.getViewInfo() == targetVi) {
+ // The node that responded is one of the selection roots.
+ // Simply invalidate the drop feedback and move on the
+ // parent in the ViewInfo chain.
+
+ updateDropFeedback(df, event);
+ mCanvas.getRulesEngine().callOnDropLeave(
+ targetNode, mCurrentDragElements, df);
+ df = null;
+ targetNode = null;
+ }
+ }
+ }
+ }
+ }
+
+ if (df == null) {
+ // Provide visual feedback that we are refusing the drop
+ event.detail = DND.DROP_NONE;
+ clearDropInfo();
+
+ } else if (targetNode != mTargetNode) {
+ // We found a new target node for the drag'n'drop.
+ // Release the previous one, if any.
+ callDropLeave();
+
+ // And assign the new one
+ mTargetNode = targetNode;
+ mFeedback = df;
+
+ // We don't need onDropMove in this case
+ isMove = false;
+ }
+ }
+
+ mCurrentView = vi;
+ }
+
+ if (isMove && mTargetNode != null && mFeedback != null) {
+ // this is a move inside the same view
+ com.android.ide.common.api.Point p2 =
+ new com.android.ide.common.api.Point(p.x, p.y);
+ updateDropFeedback(mFeedback, event);
+ DropFeedback df = mCanvas.getRulesEngine().callOnDropMove(
+ mTargetNode, mCurrentDragElements, mFeedback, p2);
+ mCanvas.getGestureManager().updateMessage(mFeedback);
+
+ if (df == null) {
+ // The target is no longer interested in the drop move.
+ event.detail = DND.DROP_NONE;
+ callDropLeave();
+
+ } else if (df != mFeedback) {
+ mFeedback = df;
+ }
+ }
+
+ if (mFeedback != null) {
+ if (event.detail == DND.DROP_NONE && !mFeedback.invalidTarget) {
+ // If we previously provided visual feedback that we were refusing
+ // the drop, we now need to change it to mean we're accepting it.
+ event.detail = DND.DROP_DEFAULT;
+ recomputeDragType(event);
+
+ } else if (mFeedback.invalidTarget) {
+ // Provide visual feedback that we are refusing the drop
+ event.detail = DND.DROP_NONE;
+ }
+ }
+
+ if (needRedraw || (mFeedback != null && mFeedback.requestPaint)) {
+ mCanvas.redraw();
+ }
+
+ // Update outline to show the target node there
+ OutlinePage outline = mCanvas.getOutlinePage();
+ TreeSelection newSelection = TreeSelection.EMPTY;
+ if (mCurrentView != null && mTargetNode != null) {
+ // Find the view corresponding to the target node. The current view can be a leaf
+ // view whereas the target node is always a parent layout.
+ if (mCurrentView.getUiViewNode() != mTargetNode.getNode()) {
+ mCurrentView = mCurrentView.getParent();
+ }
+ if (mCurrentView != null && mCurrentView.getUiViewNode() == mTargetNode.getNode()) {
+ TreePath treePath = SelectionManager.getTreePath(mCurrentView);
+ newSelection = new TreeSelection(treePath);
+ }
+ }
+
+ ISelection currentSelection = outline.getSelection();
+ if (currentSelection == null || !currentSelection.equals(newSelection)) {
+ outline.setSelection(newSelection);
+ }
+ }
+
+ /**
+ * Calls onDropLeave on mTargetNode with the current mFeedback. <br/>
+ * Then clears mTargetNode and mFeedback.
+ */
+ private void callDropLeave() {
+ if (mTargetNode != null && mFeedback != null) {
+ updateDropFeedback(mFeedback, null);
+ mCanvas.getRulesEngine().callOnDropLeave(mTargetNode, mCurrentDragElements, mFeedback);
+ }
+
+ mTargetNode = null;
+ mFeedback = null;
+ }
+
+ private void clearDropInfo() {
+ callDropLeave();
+ mCurrentView = null;
+ mCanvas.redraw();
+ }
+
+ /**
+ * Creates a root element in an empty document.
+ * Only the first element's FQCN of the dragged elements is used.
+ * <p/>
+ * Actual XML handling is done by {@link LayoutCanvas#createDocumentRoot(String)}.
+ */
+ private void createDocumentRoot(SimpleElement[] elements) {
+ if (elements == null || elements.length < 1 || elements[0] == null) {
+ return;
+ }
+
+ mCanvas.createDocumentRoot(elements[0]);
+ }
+
+ /**
+ * An {@link Overlay} to paint the move feedback. This just delegates to the
+ * layout rules.
+ */
+ private class MoveOverlay extends Overlay {
+ @Override
+ public void paint(GC gc) {
+ if (mTargetNode != null && mFeedback != null) {
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ rulesEngine.callDropFeedbackPaint(mCanvas.getGcWrapper(), mTargetNode, mFeedback);
+ mFeedback.requestPaint = false;
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDragListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDragListener.java
new file mode 100644
index 000000000..1af3053e3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDragListener.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2010 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 org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+
+import java.util.ArrayList;
+
+/** Drag listener for the outline page */
+/* package */ class OutlineDragListener implements DragSourceListener {
+ private TreeViewer mTreeViewer;
+ private OutlinePage mOutlinePage;
+ private final ArrayList<SelectionItem> mDragSelection = new ArrayList<SelectionItem>();
+ private SimpleElement[] mDragElements;
+
+ public OutlineDragListener(OutlinePage outlinePage, TreeViewer treeViewer) {
+ super();
+ mOutlinePage = outlinePage;
+ mTreeViewer = treeViewer;
+ }
+
+ @Override
+ public void dragStart(DragSourceEvent e) {
+ Tree tree = mTreeViewer.getTree();
+
+ TreeItem overTreeItem = tree.getItem(new Point(e.x, e.y));
+ if (overTreeItem == null) {
+ // Not dragging over a tree item
+ e.doit = false;
+ return;
+ }
+ CanvasViewInfo over = getViewInfo(overTreeItem);
+ if (over == null) {
+ e.doit = false;
+ return;
+ }
+
+ // The selection logic for the outline is much simpler than in the canvas,
+ // because for one thing, the tree selection is updated synchronously on mouse
+ // down, so it's not possible to start dragging a non-selected item.
+ // We also don't deliberately disallow root-element dragging since you can
+ // drag it into another form.
+ final LayoutCanvas canvas = mOutlinePage.getEditor().getCanvasControl();
+ SelectionManager selectionManager = canvas.getSelectionManager();
+ TreeItem[] treeSelection = tree.getSelection();
+ mDragSelection.clear();
+ for (TreeItem item : treeSelection) {
+ CanvasViewInfo viewInfo = getViewInfo(item);
+ if (viewInfo != null) {
+ mDragSelection.add(selectionManager.createSelection(viewInfo));
+ }
+ }
+ SelectionManager.sanitize(mDragSelection);
+
+ e.doit = !mDragSelection.isEmpty();
+ int imageCount = mDragSelection.size();
+ if (e.doit) {
+ mDragElements = SelectionItem.getAsElements(mDragSelection);
+ GlobalCanvasDragInfo.getInstance().startDrag(mDragElements,
+ mDragSelection.toArray(new SelectionItem[imageCount]),
+ canvas, new Runnable() {
+ @Override
+ public void run() {
+ canvas.getClipboardSupport().deleteSelection("Remove",
+ mDragSelection);
+ }
+ });
+ return;
+ }
+
+ e.detail = DND.DROP_NONE;
+ }
+
+ @Override
+ public void dragSetData(DragSourceEvent e) {
+ if (TextTransfer.getInstance().isSupportedType(e.dataType)) {
+ LayoutCanvas canvas = mOutlinePage.getEditor().getCanvasControl();
+ e.data = SelectionItem.getAsText(canvas, mDragSelection);
+ return;
+ }
+
+ if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) {
+ e.data = mDragElements;
+ return;
+ }
+
+ // otherwise we failed
+ e.detail = DND.DROP_NONE;
+ e.doit = false;
+ }
+
+ @Override
+ public void dragFinished(DragSourceEvent e) {
+ // Unregister the dragged data.
+ // Clear the selection
+ mDragSelection.clear();
+ mDragElements = null;
+ GlobalCanvasDragInfo.getInstance().stopDrag();
+ }
+
+ private CanvasViewInfo getViewInfo(TreeItem item) {
+ Object data = item.getData();
+ if (data != null) {
+ return OutlinePage.getViewInfo(data);
+ }
+
+ return null;
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDropListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDropListener.java
new file mode 100644
index 000000000..f4a826fa2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineDropListener.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2010 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.ide.common.api.INode;
+import com.android.ide.common.api.InsertType;
+import com.android.ide.common.layout.BaseLayoutRule;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.ViewerDropAdapter;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.TransferData;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** Drop listener for the outline page */
+/*package*/ class OutlineDropListener extends ViewerDropAdapter {
+ private final OutlinePage mOutlinePage;
+
+ public OutlineDropListener(OutlinePage outlinePage, TreeViewer treeViewer) {
+ super(treeViewer);
+ mOutlinePage = outlinePage;
+ }
+
+ @Override
+ public void dragEnter(DropTargetEvent event) {
+ if (event.detail == DND.DROP_NONE && GlobalCanvasDragInfo.getInstance().isDragging()) {
+ // For some inexplicable reason, we get DND.DROP_NONE from the palette
+ // even though in its drag start we set DND.DROP_COPY, so correct that here...
+ int operation = DND.DROP_COPY;
+ event.detail = operation;
+ }
+ super.dragEnter(event);
+ }
+
+ @Override
+ public boolean performDrop(Object data) {
+ final DropTargetEvent event = getCurrentEvent();
+ if (event == null) {
+ return false;
+ }
+ int location = determineLocation(event);
+ if (location == LOCATION_NONE) {
+ return false;
+ }
+
+ final SimpleElement[] elements;
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+ if (sxt.isSupportedType(event.currentDataType)) {
+ if (data instanceof SimpleElement[]) {
+ elements = (SimpleElement[]) data;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ if (elements.length == 0) {
+ return false;
+ }
+
+ // Determine target:
+ CanvasViewInfo parent = OutlinePage.getViewInfo(event.item.getData());
+ if (parent == null) {
+ return false;
+ }
+
+ int index = -1;
+ UiViewElementNode parentNode = parent.getUiViewNode();
+ if (location == LOCATION_BEFORE || location == LOCATION_AFTER) {
+ UiViewElementNode node = parentNode;
+ parent = parent.getParent();
+ if (parent == null) {
+ return false;
+ }
+ parentNode = parent.getUiViewNode();
+
+ // Determine index
+ index = 0;
+ for (UiElementNode child : parentNode.getUiChildren()) {
+ if (child == node) {
+ break;
+ }
+ index++;
+ }
+ if (location == LOCATION_AFTER) {
+ index++;
+ }
+ }
+
+ // Copy into new position.
+ final LayoutCanvas canvas = mOutlinePage.getEditor().getCanvasControl();
+ final NodeProxy targetNode = canvas.getNodeFactory().create(parentNode);
+
+ // Record children of the target right before the drop (such that we can
+ // find out after the drop which exact children were inserted)
+ Set<INode> children = new HashSet<INode>();
+ for (INode node : targetNode.getChildren()) {
+ children.add(node);
+ }
+
+ String label = MoveGesture.computeUndoLabel(targetNode, elements, event.detail);
+ final int indexFinal = index;
+ canvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(label, new Runnable() {
+ @Override
+ public void run() {
+ InsertType insertType = MoveGesture.getInsertType(event, targetNode);
+ canvas.getRulesEngine().setInsertType(insertType);
+
+ Object sourceCanvas = GlobalCanvasDragInfo.getInstance().getSourceCanvas();
+ boolean createNew = event.detail == DND.DROP_COPY || sourceCanvas != canvas;
+ BaseLayoutRule.insertAt(targetNode, elements, createNew, indexFinal);
+ targetNode.applyPendingChanges();
+
+ // Clean up drag if applicable
+ if (event.detail == DND.DROP_MOVE) {
+ GlobalCanvasDragInfo.getInstance().removeSource();
+ }
+ }
+ });
+
+ // Now find out which nodes were added, and look up their corresponding
+ // CanvasViewInfos
+ final List<INode> added = new ArrayList<INode>();
+ for (INode node : targetNode.getChildren()) {
+ if (!children.contains(node)) {
+ added.add(node);
+ }
+ }
+ // Select the newly dropped nodes
+ final SelectionManager selectionManager = canvas.getSelectionManager();
+ selectionManager.setOutlineSelection(added);
+
+ canvas.redraw();
+
+ return true;
+ }
+
+ @Override
+ public boolean validateDrop(Object target, int operation,
+ TransferData transferType) {
+ DropTargetEvent event = getCurrentEvent();
+ if (event == null) {
+ return false;
+ }
+ int location = determineLocation(event);
+ if (location == LOCATION_NONE) {
+ return false;
+ }
+
+ SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+ if (!sxt.isSupportedType(transferType)) {
+ return false;
+ }
+
+ CanvasViewInfo parent = OutlinePage.getViewInfo(event.item.getData());
+ if (parent == null) {
+ return false;
+ }
+
+ UiViewElementNode parentNode = parent.getUiViewNode();
+
+ if (location == LOCATION_ON) {
+ // Targeting the middle of an item means to add it as a new child
+ // of the given element. This is only allowed on some types of nodes.
+ if (!DescriptorsUtils.canInsertChildren(parentNode.getDescriptor(),
+ parent.getViewObject())) {
+ return false;
+ }
+ }
+
+ // Check that the drop target position is not a child or identical to
+ // one of the dragged items
+ SelectionItem[] sel = GlobalCanvasDragInfo.getInstance().getCurrentSelection();
+ if (sel != null) {
+ for (SelectionItem item : sel) {
+ if (isAncestor(item.getViewInfo().getUiViewNode(), parentNode)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /** Returns true if the given parent node is an ancestor of the given child node */
+ private boolean isAncestor(UiElementNode parent, UiElementNode child) {
+ while (child != null) {
+ if (child == parent) {
+ return true;
+ }
+ child = child.getUiParent();
+ }
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineOverlay.java
new file mode 100644
index 000000000..e63fff7ab
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineOverlay.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2010 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 org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+
+/**
+ * The {@link OutlineOverlay} paints an optional outline on top of the layout,
+ * showing the structure of the individual Android View elements.
+ */
+public class OutlineOverlay extends Overlay {
+ /** The {@link ViewHierarchy} this outline visualizes */
+ private final ViewHierarchy mViewHierarchy;
+
+ /** Outline color. Must be disposed, it's NOT a system color. */
+ private Color mOutlineColor;
+
+ /** Vertical scaling & scrollbar information. */
+ private CanvasTransform mVScale;
+
+ /** Horizontal scaling & scrollbar information. */
+ private CanvasTransform mHScale;
+
+ /**
+ * Constructs a new {@link OutlineOverlay} linked to the given view
+ * hierarchy.
+ *
+ * @param viewHierarchy The {@link ViewHierarchy} to render
+ * @param hScale The {@link CanvasTransform} to use to transfer horizontal layout
+ * coordinates to screen coordinates
+ * @param vScale The {@link CanvasTransform} to use to transfer vertical layout
+ * coordinates to screen coordinates
+ */
+ public OutlineOverlay(
+ ViewHierarchy viewHierarchy,
+ CanvasTransform hScale,
+ CanvasTransform vScale) {
+ super();
+ mViewHierarchy = viewHierarchy;
+ mHScale = hScale;
+ mVScale = vScale;
+ }
+
+ @Override
+ public void create(Device device) {
+ mOutlineColor = new Color(device, SwtDrawingStyle.OUTLINE.getStrokeColor());
+ }
+
+ @Override
+ public void dispose() {
+ if (mOutlineColor != null) {
+ mOutlineColor.dispose();
+ mOutlineColor = null;
+ }
+ }
+
+ @Override
+ public void paint(GC gc) {
+ CanvasViewInfo lastRoot = mViewHierarchy.getRoot();
+ if (lastRoot != null) {
+ gc.setForeground(mOutlineColor);
+ gc.setLineStyle(SwtDrawingStyle.OUTLINE.getLineStyle());
+ int oldAlpha = gc.getAlpha();
+ gc.setAlpha(SwtDrawingStyle.OUTLINE.getStrokeAlpha());
+ drawOutline(gc, lastRoot);
+ gc.setAlpha(oldAlpha);
+ }
+ }
+
+ private void drawOutline(GC gc, CanvasViewInfo info) {
+ Rectangle r = info.getAbsRect();
+
+ int x = mHScale.translate(r.x);
+ int y = mVScale.translate(r.y);
+ int w = mHScale.scale(r.width);
+ int h = mVScale.scale(r.height);
+
+ // Add +1 to the width and +1 to the height such that when you have a
+ // series of boxes (in say a LinearLayout), instead of the bottom of one
+ // box and the top of the next box being -adjacent-, they -overlap-.
+ // This makes the outline nicer visually since you don't get
+ // "double thickness" lines for all adjacent boxes.
+ gc.drawRectangle(x, y, w + 1, h + 1);
+
+ for (CanvasViewInfo vi : info.getChildren()) {
+ drawOutline(gc, vi);
+ }
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java
new file mode 100644
index 000000000..8178c6871
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java
@@ -0,0 +1,1439 @@
+/*
+ * Copyright (C) 2010 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 static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_COLUMN_COUNT;
+import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
+import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
+import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
+import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
+import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
+import static com.android.SdkConstants.ATTR_ROW_COUNT;
+import static com.android.SdkConstants.ATTR_SRC;
+import static com.android.SdkConstants.ATTR_TEXT;
+import static com.android.SdkConstants.AUTO_URI;
+import static com.android.SdkConstants.DRAWABLE_PREFIX;
+import static com.android.SdkConstants.GRID_LAYOUT;
+import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.URI_PREFIX;
+import static org.eclipse.jface.viewers.StyledString.COUNTER_STYLER;
+import static org.eclipse.jface.viewers.StyledString.QUALIFIER_STYLER;
+
+import com.android.SdkConstants;
+import com.android.annotations.VisibleForTesting;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.InsertType;
+import com.android.ide.common.layout.BaseLayoutRule;
+import com.android.ide.common.layout.GridLayoutRule;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+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.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertySheetPage;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+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.IMenuListener;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.IToolBarManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.preference.JFacePreferences;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.IElementComparer;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StyledCellLabelProvider;
+import org.eclipse.jface.viewers.StyledString;
+import org.eclipse.jface.viewers.StyledString.Styler;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerCell;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MenuDetectEvent;
+import org.eclipse.swt.events.MenuDetectListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.INullSelectionListener;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.actions.ActionFactory;
+import org.eclipse.ui.views.contentoutline.ContentOutlinePage;
+import org.eclipse.wb.core.controls.SelfOrientingSashForm;
+import org.eclipse.wb.internal.core.editor.structure.IPage;
+import org.eclipse.wb.internal.core.editor.structure.PageSiteComposite;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An outline page for the layout canvas view.
+ * <p/>
+ * The page is created by {@link LayoutEditorDelegate#delegateGetAdapter(Class)}. This means
+ * we have *one* instance of the outline page per open canvas editor.
+ * <p/>
+ * It sets itself as a listener on the site's selection service in order to be
+ * notified of the canvas' selection changes.
+ * The underlying page is also a selection provider (via IContentOutlinePage)
+ * and as such it will broadcast selection changes to the site's selection service
+ * (on which both the layout editor part and the property sheet page listen.)
+ */
+public class OutlinePage extends ContentOutlinePage
+ implements INullSelectionListener, IPage {
+
+ /** Label which separates outline text from additional attributes like text prefix or url */
+ private static final String LABEL_SEPARATOR = " - ";
+
+ /** Max character count in labels, used for truncation */
+ private static final int LABEL_MAX_WIDTH = 50;
+
+ /**
+ * The graphical editor that created this outline.
+ */
+ private final GraphicalEditorPart mGraphicalEditorPart;
+
+ /**
+ * RootWrapper is a workaround: we can't set the input of the TreeView to its root
+ * element, so we introduce a fake parent.
+ */
+ private final RootWrapper mRootWrapper = new RootWrapper();
+
+ /**
+ * Menu manager for the context menu actions.
+ * The actions delegate to the current GraphicalEditorPart.
+ */
+ private MenuManager mMenuManager;
+
+ private Composite mControl;
+ private PropertySheetPage mPropertySheet;
+ private PageSiteComposite mPropertySheetComposite;
+ private boolean mShowPropertySheet;
+ private boolean mShowHeader;
+ private boolean mIgnoreSelection;
+ private boolean mActive = true;
+
+ /** Action to Select All in the tree */
+ private final Action mTreeSelectAllAction = new Action() {
+ @Override
+ public void run() {
+ getTreeViewer().getTree().selectAll();
+ OutlinePage.this.fireSelectionChanged(getSelection());
+ }
+
+ @Override
+ public String getId() {
+ return ActionFactory.SELECT_ALL.getId();
+ }
+ };
+
+ /** Action for moving items up in the tree */
+ private Action mMoveUpAction = new Action("Move Up\t-",
+ IconFactory.getInstance().getImageDescriptor("up")) { //$NON-NLS-1$
+
+ @Override
+ public String getId() {
+ return "adt.outline.moveup"; //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return canMove(false);
+ }
+
+ @Override
+ public void run() {
+ move(false);
+ }
+ };
+
+ /** Action for moving items down in the tree */
+ private Action mMoveDownAction = new Action("Move Down\t+",
+ IconFactory.getInstance().getImageDescriptor("down")) { //$NON-NLS-1$
+
+ @Override
+ public String getId() {
+ return "adt.outline.movedown"; //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return canMove(true);
+ }
+
+ @Override
+ public void run() {
+ move(true);
+ }
+ };
+
+ /**
+ * Creates a new {@link OutlinePage} associated with the given editor
+ *
+ * @param graphicalEditorPart the editor associated with this outline
+ */
+ public OutlinePage(GraphicalEditorPart graphicalEditorPart) {
+ super();
+ mGraphicalEditorPart = graphicalEditorPart;
+ }
+
+ @Override
+ public Control getControl() {
+ // We've injected some controls between the root of the outline page
+ // and the tree control, so return the actual root (a sash form) rather
+ // than the superclass' implementation which returns the tree. If we don't
+ // do this, various checks in the outline page which checks that getControl().getParent()
+ // is the outline window itself will ignore this page.
+ return mControl;
+ }
+
+ void setActive(boolean active) {
+ if (active != mActive) {
+ mActive = active;
+
+ // Outlines are by default active when they are created; this is intended
+ // for deactivating a hidden outline and later reactivating it
+ assert mControl != null;
+ if (active) {
+ getSite().getPage().addSelectionListener(this);
+ setModel(mGraphicalEditorPart.getCanvasControl().getViewHierarchy().getRoot());
+ } else {
+ getSite().getPage().removeSelectionListener(this);
+ mRootWrapper.setRoot(null);
+ if (mPropertySheet != null) {
+ mPropertySheet.selectionChanged(null, TreeSelection.EMPTY);
+ }
+ }
+ }
+ }
+
+ /** Refresh all the icon state */
+ public void refreshIcons() {
+ TreeViewer treeViewer = getTreeViewer();
+ if (treeViewer != null) {
+ Tree tree = treeViewer.getTree();
+ if (tree != null && !tree.isDisposed()) {
+ treeViewer.refresh();
+ }
+ }
+ }
+
+ /**
+ * Set whether the outline should be shown in the header
+ *
+ * @param show whether a header should be shown
+ */
+ public void setShowHeader(boolean show) {
+ mShowHeader = show;
+ }
+
+ /**
+ * Set whether the property sheet should be shown within this outline
+ *
+ * @param show whether the property sheet should show
+ */
+ public void setShowPropertySheet(boolean show) {
+ if (show != mShowPropertySheet) {
+ mShowPropertySheet = show;
+ if (mControl == null) {
+ return;
+ }
+
+ if (show && mPropertySheet == null) {
+ createPropertySheet();
+ } else if (!show) {
+ mPropertySheetComposite.dispose();
+ mPropertySheetComposite = null;
+ mPropertySheet.dispose();
+ mPropertySheet = null;
+ }
+
+ mControl.layout();
+ }
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ mControl = new SelfOrientingSashForm(parent, SWT.VERTICAL);
+
+ if (mShowHeader) {
+ PageSiteComposite mOutlineComposite = new PageSiteComposite(mControl, SWT.BORDER);
+ mOutlineComposite.setTitleText("Outline");
+ mOutlineComposite.setTitleImage(IconFactory.getInstance().getIcon("components_view"));
+ mOutlineComposite.setPage(new IPage() {
+ @Override
+ public void createControl(Composite outlineParent) {
+ createOutline(outlineParent);
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public Control getControl() {
+ return getTreeViewer().getTree();
+ }
+
+ @Override
+ public void setToolBar(IToolBarManager toolBarManager) {
+ makeContributions(null, toolBarManager, null);
+ toolBarManager.update(false);
+ }
+
+ @Override
+ public void setFocus() {
+ getControl().setFocus();
+ }
+ });
+ } else {
+ createOutline(mControl);
+ }
+
+ if (mShowPropertySheet) {
+ createPropertySheet();
+ }
+ }
+
+ private void createOutline(Composite parent) {
+ if (AdtUtils.isEclipse4()) {
+ // This is a workaround for the focus behavior in Eclipse 4 where
+ // the framework ends up calling setFocus() on the first widget in the outline
+ // AFTER a mouse click has been received. Specifically, if the user clicks in
+ // the embedded property sheet to for example give a Text property editor focus,
+ // then after the mouse click, the Outline window activation event is processed,
+ // and this event causes setFocus() to be called first on the PageBookView (which
+ // ends up calling setFocus on the first control, normally the TreeViewer), and
+ // then on the Page itself. We're dealing with the page setFocus() in the override
+ // of that method in the class, such that it does nothing.
+ // However, we have to also disable the setFocus on the first control in the
+ // outline page. To deal with that, we create our *own* first control in the
+ // outline, and make its setFocus() a no-op. We also make it invisible, since we
+ // don't actually want anything but the tree viewer showing in the outline.
+ Text text = new Text(parent, SWT.NONE) {
+ @Override
+ public boolean setFocus() {
+ // Focus no-op
+ return true;
+ }
+
+ @Override
+ protected void checkSubclass() {
+ // Disable the check that prevents subclassing of SWT components
+ }
+ };
+ text.setVisible(false);
+ }
+
+ super.createControl(parent);
+
+ TreeViewer tv = getTreeViewer();
+ tv.setAutoExpandLevel(2);
+ tv.setContentProvider(new ContentProvider());
+ tv.setLabelProvider(new LabelProvider());
+ tv.setInput(mRootWrapper);
+ tv.expandToLevel(mRootWrapper.getRoot(), 2);
+
+ int supportedOperations = DND.DROP_COPY | DND.DROP_MOVE;
+ Transfer[] transfers = new Transfer[] {
+ SimpleXmlTransfer.getInstance()
+ };
+
+ tv.addDropSupport(supportedOperations, transfers, new OutlineDropListener(this, tv));
+ tv.addDragSupport(supportedOperations, transfers, new OutlineDragListener(this, tv));
+
+ // The tree viewer will hold CanvasViewInfo instances, however these
+ // change each time the canvas is reloaded. OTOH layoutlib gives us
+ // constant UiView keys which we can use to perform tree item comparisons.
+ tv.setComparer(new IElementComparer() {
+ @Override
+ public int hashCode(Object element) {
+ if (element instanceof CanvasViewInfo) {
+ UiViewElementNode key = ((CanvasViewInfo) element).getUiViewNode();
+ if (key != null) {
+ return key.hashCode();
+ }
+ }
+ if (element != null) {
+ return element.hashCode();
+ }
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object a, Object b) {
+ if (a instanceof CanvasViewInfo && b instanceof CanvasViewInfo) {
+ UiViewElementNode keyA = ((CanvasViewInfo) a).getUiViewNode();
+ UiViewElementNode keyB = ((CanvasViewInfo) b).getUiViewNode();
+ if (keyA != null) {
+ return keyA.equals(keyB);
+ }
+ }
+ if (a != null) {
+ return a.equals(b);
+ }
+ return false;
+ }
+ });
+ tv.addDoubleClickListener(new IDoubleClickListener() {
+ @Override
+ public void doubleClick(DoubleClickEvent event) {
+ // This used to open the property view, but now that properties are docked
+ // let's use it for something else -- such as showing the editor source
+ /*
+ // Front properties panel; its selection is already linked
+ IWorkbenchPage page = getSite().getPage();
+ try {
+ page.showView(IPageLayout.ID_PROP_SHEET, null, IWorkbenchPage.VIEW_ACTIVATE);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Could not activate property sheet");
+ }
+ */
+
+ TreeItem[] selection = getTreeViewer().getTree().getSelection();
+ if (selection.length > 0) {
+ CanvasViewInfo vi = getViewInfo(selection[0].getData());
+ if (vi != null) {
+ LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
+ canvas.show(vi);
+ }
+ }
+ }
+ });
+
+ setupContextMenu();
+
+ // Listen to selection changes from the layout editor
+ getSite().getPage().addSelectionListener(this);
+ getControl().addDisposeListener(new DisposeListener() {
+
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ dispose();
+ }
+ });
+
+ Tree tree = tv.getTree();
+ tree.addKeyListener(new KeyListener() {
+
+ @Override
+ public void keyPressed(KeyEvent e) {
+ if (e.character == '-') {
+ if (mMoveUpAction.isEnabled()) {
+ mMoveUpAction.run();
+ }
+ } else if (e.character == '+') {
+ if (mMoveDownAction.isEnabled()) {
+ mMoveDownAction.run();
+ }
+ }
+ }
+
+ @Override
+ public void keyReleased(KeyEvent e) {
+ }
+ });
+
+ setupTooltip();
+ }
+
+ /**
+ * This flag is true when the mouse button is being pressed somewhere inside
+ * the property sheet
+ */
+ private boolean mPressInPropSheet;
+
+ private void createPropertySheet() {
+ mPropertySheetComposite = new PageSiteComposite(mControl, SWT.BORDER);
+ mPropertySheetComposite.setTitleText("Properties");
+ mPropertySheetComposite.setTitleImage(IconFactory.getInstance().getIcon("properties_view"));
+ mPropertySheet = new PropertySheetPage(mGraphicalEditorPart);
+ mPropertySheetComposite.setPage(mPropertySheet);
+ if (AdtUtils.isEclipse4()) {
+ mPropertySheet.getControl().addMouseListener(new MouseListener() {
+ @Override
+ public void mouseDown(MouseEvent e) {
+ mPressInPropSheet = true;
+ }
+
+ @Override
+ public void mouseUp(MouseEvent e) {
+ mPressInPropSheet = false;
+ }
+
+ @Override
+ public void mouseDoubleClick(MouseEvent e) {
+ }
+ });
+ }
+ }
+
+ @Override
+ public void setFocus() {
+ // Only call setFocus on the tree viewer if the mouse click isn't in the property
+ // sheet area
+ if (!mPressInPropSheet) {
+ super.setFocus();
+ }
+ }
+
+ @Override
+ public void dispose() {
+ mRootWrapper.setRoot(null);
+
+ getSite().getPage().removeSelectionListener(this);
+ super.dispose();
+ if (mPropertySheet != null) {
+ mPropertySheet.dispose();
+ mPropertySheet = null;
+ }
+ }
+
+ /**
+ * Invoked by {@link LayoutCanvas} to set the model (a.k.a. the root view info).
+ *
+ * @param rootViewInfo The root of the view info hierarchy. Can be null.
+ */
+ public void setModel(CanvasViewInfo rootViewInfo) {
+ if (!mActive) {
+ return;
+ }
+
+ mRootWrapper.setRoot(rootViewInfo);
+
+ TreeViewer tv = getTreeViewer();
+ if (tv != null && !tv.getTree().isDisposed()) {
+ Object[] expanded = tv.getExpandedElements();
+ tv.refresh();
+ tv.setExpandedElements(expanded);
+ // Ensure that the root is expanded
+ tv.expandToLevel(rootViewInfo, 2);
+ }
+ }
+
+ /**
+ * Returns the current tree viewer selection. Shouldn't be null,
+ * although it can be {@link TreeSelection#EMPTY}.
+ */
+ @Override
+ public ISelection getSelection() {
+ return super.getSelection();
+ }
+
+ /**
+ * Sets the outline selection.
+ *
+ * @param selection Only {@link ITreeSelection} will be used, otherwise the
+ * selection will be cleared (including a null selection).
+ */
+ @Override
+ public void setSelection(ISelection selection) {
+ // TreeViewer should be able to deal with a null selection, but let's make it safe
+ if (selection == null) {
+ selection = TreeSelection.EMPTY;
+ }
+ if (selection.equals(TreeSelection.EMPTY)) {
+ return;
+ }
+
+ super.setSelection(selection);
+
+ TreeViewer tv = getTreeViewer();
+ if (tv == null || !(selection instanceof ITreeSelection) || selection.isEmpty()) {
+ return;
+ }
+
+ // auto-reveal the selection
+ ITreeSelection treeSel = (ITreeSelection) selection;
+ for (TreePath p : treeSel.getPaths()) {
+ tv.expandToLevel(p, 1);
+ }
+ }
+
+ @Override
+ protected void fireSelectionChanged(ISelection selection) {
+ super.fireSelectionChanged(selection);
+ if (mPropertySheet != null && !mIgnoreSelection) {
+ mPropertySheet.selectionChanged(null, selection);
+ }
+ }
+
+ /**
+ * Listens to a workbench selection.
+ * Only listen on selection coming from {@link LayoutEditorDelegate}, which avoid
+ * picking up our own selections.
+ */
+ @Override
+ public void selectionChanged(IWorkbenchPart part, ISelection selection) {
+ if (mIgnoreSelection) {
+ return;
+ }
+
+ if (part instanceof IEditorPart) {
+ LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor((IEditorPart) part);
+ if (delegate != null) {
+ try {
+ mIgnoreSelection = true;
+ setSelection(selection);
+
+ if (mPropertySheet != null) {
+ mPropertySheet.selectionChanged(part, selection);
+ }
+ } finally {
+ mIgnoreSelection = false;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ if (!mIgnoreSelection) {
+ super.selectionChanged(event);
+ }
+ }
+
+ // ----
+
+ /**
+ * In theory, the root of the model should be the input of the {@link TreeViewer},
+ * which would be the root {@link CanvasViewInfo}.
+ * That means in theory {@link ContentProvider#getElements(Object)} should return
+ * its own input as the single root node.
+ * <p/>
+ * However as described in JFace Bug 9262, this case is not properly handled by
+ * a {@link TreeViewer} and leads to an infinite recursion in the tree viewer.
+ * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=9262
+ * <p/>
+ * The solution is to wrap the tree viewer input in a dummy root node that acts
+ * as a parent. This class does just that.
+ */
+ private static class RootWrapper {
+ private CanvasViewInfo mRoot;
+
+ public void setRoot(CanvasViewInfo root) {
+ mRoot = root;
+ }
+
+ public CanvasViewInfo getRoot() {
+ return mRoot;
+ }
+ }
+
+ /** Return the {@link CanvasViewInfo} associated with the given TreeItem's data field */
+ /* package */ static CanvasViewInfo getViewInfo(Object viewData) {
+ if (viewData instanceof RootWrapper) {
+ return ((RootWrapper) viewData).getRoot();
+ }
+ if (viewData instanceof CanvasViewInfo) {
+ return (CanvasViewInfo) viewData;
+ }
+ return null;
+ }
+
+ // --- Content and Label Providers ---
+
+ /**
+ * Content provider for the Outline model.
+ * Objects are going to be {@link CanvasViewInfo}.
+ */
+ private static class ContentProvider implements ITreeContentProvider {
+
+ @Override
+ public Object[] getChildren(Object element) {
+ if (element instanceof RootWrapper) {
+ CanvasViewInfo root = ((RootWrapper)element).getRoot();
+ if (root != null) {
+ return new Object[] { root };
+ }
+ }
+ if (element instanceof CanvasViewInfo) {
+ List<CanvasViewInfo> children = ((CanvasViewInfo) element).getUniqueChildren();
+ if (children != null) {
+ return children.toArray();
+ }
+ }
+ return new Object[0];
+ }
+
+ @Override
+ public Object getParent(Object element) {
+ if (element instanceof CanvasViewInfo) {
+ return ((CanvasViewInfo) element).getParent();
+ }
+ return null;
+ }
+
+ @Override
+ public boolean hasChildren(Object element) {
+ if (element instanceof CanvasViewInfo) {
+ List<CanvasViewInfo> children = ((CanvasViewInfo) element).getChildren();
+ if (children != null) {
+ return children.size() > 0;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the root element.
+ * Semantically, the root element is the single top-level XML element of the XML layout.
+ */
+ @Override
+ public Object[] getElements(Object inputElement) {
+ return getChildren(inputElement);
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+ }
+
+ /**
+ * Label provider for the Outline model.
+ * Objects are going to be {@link CanvasViewInfo}.
+ */
+ private class LabelProvider extends StyledCellLabelProvider {
+ /**
+ * Returns the element's logo with a fallback on the android logo.
+ *
+ * @param element the tree element
+ * @return the image to be used as a logo
+ */
+ public Image getImage(Object element) {
+ if (element instanceof CanvasViewInfo) {
+ element = ((CanvasViewInfo) element).getUiViewNode();
+ }
+
+ if (element instanceof UiViewElementNode) {
+ UiViewElementNode v = (UiViewElementNode) element;
+ return v.getIcon();
+ }
+
+ return AdtPlugin.getAndroidLogo();
+ }
+
+ /**
+ * Uses {@link UiElementNode#getStyledDescription} for the label for this tree item.
+ */
+ @Override
+ public void update(ViewerCell cell) {
+ Object element = cell.getElement();
+ StyledString styledString = null;
+
+ CanvasViewInfo vi = null;
+ if (element instanceof CanvasViewInfo) {
+ vi = (CanvasViewInfo) element;
+ element = vi.getUiViewNode();
+ }
+
+ Image image = getImage(element);
+
+ if (element instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) element;
+ styledString = node.getStyledDescription();
+ Node xmlNode = node.getXmlNode();
+ if (xmlNode instanceof Element) {
+ Element e = (Element) xmlNode;
+
+ // Temporary diagnostics code when developing GridLayout
+ if (GridLayoutRule.sDebugGridLayout) {
+
+ String namespace;
+ if (e.getNodeName().equals(GRID_LAYOUT) ||
+ e.getParentNode() != null
+ && e.getParentNode().getNodeName().equals(GRID_LAYOUT)) {
+ namespace = ANDROID_URI;
+ } else {
+ // Else: probably a v7 gridlayout
+ IProject project = mGraphicalEditorPart.getProject();
+ ProjectState projectState = Sdk.getProjectState(project);
+ if (projectState != null && projectState.isLibrary()) {
+ namespace = AUTO_URI;
+ } else {
+ ManifestInfo info = ManifestInfo.get(project);
+ namespace = URI_PREFIX + info.getPackage();
+ }
+ }
+
+ if (e.getNodeName() != null && e.getNodeName().endsWith(GRID_LAYOUT)) {
+ // Attach rowCount/columnCount info
+ String rowCount = e.getAttributeNS(namespace, ATTR_ROW_COUNT);
+ if (rowCount.length() == 0) {
+ rowCount = "?";
+ }
+ String columnCount = e.getAttributeNS(namespace, ATTR_COLUMN_COUNT);
+ if (columnCount.length() == 0) {
+ columnCount = "?";
+ }
+
+ styledString.append(" - columnCount=", QUALIFIER_STYLER);
+ styledString.append(columnCount, QUALIFIER_STYLER);
+ styledString.append(", rowCount=", QUALIFIER_STYLER);
+ styledString.append(rowCount, QUALIFIER_STYLER);
+ } else if (e.getParentNode() != null
+ && e.getParentNode().getNodeName() != null
+ && e.getParentNode().getNodeName().endsWith(GRID_LAYOUT)) {
+ // Attach row/column info
+ String row = e.getAttributeNS(namespace, ATTR_LAYOUT_ROW);
+ if (row.length() == 0) {
+ row = "?";
+ }
+ Styler colStyle = QUALIFIER_STYLER;
+ String column = e.getAttributeNS(namespace, ATTR_LAYOUT_COLUMN);
+ if (column.length() == 0) {
+ column = "?";
+ } else {
+ String colCount = ((Element) e.getParentNode()).getAttributeNS(
+ namespace, ATTR_COLUMN_COUNT);
+ if (colCount.length() > 0 && Integer.parseInt(colCount) <=
+ Integer.parseInt(column)) {
+ colStyle = StyledString.createColorRegistryStyler(
+ JFacePreferences.ERROR_COLOR, null);
+ }
+ }
+ String rowSpan = e.getAttributeNS(namespace, ATTR_LAYOUT_ROW_SPAN);
+ String columnSpan = e.getAttributeNS(namespace,
+ ATTR_LAYOUT_COLUMN_SPAN);
+ if (rowSpan.length() == 0) {
+ rowSpan = "1";
+ }
+ if (columnSpan.length() == 0) {
+ columnSpan = "1";
+ }
+
+ styledString.append(" - cell (row=", QUALIFIER_STYLER);
+ styledString.append(row, QUALIFIER_STYLER);
+ styledString.append(',', QUALIFIER_STYLER);
+ styledString.append("col=", colStyle);
+ styledString.append(column, colStyle);
+ styledString.append(')', colStyle);
+ styledString.append(", span=(", QUALIFIER_STYLER);
+ styledString.append(columnSpan, QUALIFIER_STYLER);
+ styledString.append(',', QUALIFIER_STYLER);
+ styledString.append(rowSpan, QUALIFIER_STYLER);
+ styledString.append(')', QUALIFIER_STYLER);
+
+ String gravity = e.getAttributeNS(namespace, ATTR_LAYOUT_GRAVITY);
+ if (gravity != null && gravity.length() > 0) {
+ styledString.append(" : ", COUNTER_STYLER);
+ styledString.append(gravity, COUNTER_STYLER);
+ }
+
+ }
+ }
+
+ if (e.hasAttributeNS(ANDROID_URI, ATTR_TEXT)) {
+ // Show the text attribute
+ String text = e.getAttributeNS(ANDROID_URI, ATTR_TEXT);
+ if (text != null && text.length() > 0
+ && !text.contains(node.getDescriptor().getUiName())) {
+ if (text.charAt(0) == '@') {
+ String resolved = mGraphicalEditorPart.findString(text);
+ if (resolved != null) {
+ text = resolved;
+ }
+ }
+ if (styledString.length() < LABEL_MAX_WIDTH - LABEL_SEPARATOR.length()
+ - 2) {
+ styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
+
+ styledString.append('"', QUALIFIER_STYLER);
+ styledString.append(truncate(text, styledString), QUALIFIER_STYLER);
+ styledString.append('"', QUALIFIER_STYLER);
+ }
+ }
+ } else if (e.hasAttributeNS(ANDROID_URI, ATTR_SRC)) {
+ // Show ImageView source attributes etc
+ String src = e.getAttributeNS(ANDROID_URI, ATTR_SRC);
+ if (src != null && src.length() > 0) {
+ if (src.startsWith(DRAWABLE_PREFIX)) {
+ src = src.substring(DRAWABLE_PREFIX.length());
+ }
+ styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
+ styledString.append(truncate(src, styledString), QUALIFIER_STYLER);
+ }
+ } else if (e.getTagName().equals(SdkConstants.VIEW_INCLUDE)) {
+ // Show the include reference.
+
+ // Note: the layout attribute is NOT in the Android namespace
+ String src = e.getAttribute(SdkConstants.ATTR_LAYOUT);
+ if (src != null && src.length() > 0) {
+ if (src.startsWith(LAYOUT_RESOURCE_PREFIX)) {
+ src = src.substring(LAYOUT_RESOURCE_PREFIX.length());
+ }
+ styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
+ styledString.append(truncate(src, styledString), QUALIFIER_STYLER);
+ }
+ }
+ }
+ } else if (element == null && vi != null) {
+ // It's an inclusion-context: display it
+ Reference includedWithin = mGraphicalEditorPart.getIncludedWithin();
+ if (includedWithin != null) {
+ styledString = new StyledString();
+ styledString.append(includedWithin.getDisplayName(), QUALIFIER_STYLER);
+ image = IconFactory.getInstance().getIcon(SdkConstants.VIEW_INCLUDE);
+ }
+ }
+
+ if (styledString == null) {
+ styledString = new StyledString();
+ styledString.append(element == null ? "(null)" : element.toString());
+ }
+
+ cell.setText(styledString.toString());
+ cell.setStyleRanges(styledString.getStyleRanges());
+ cell.setImage(image);
+ super.update(cell);
+ }
+
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ return super.isLabelProperty(element, property);
+ }
+ }
+
+ // --- Context Menu ---
+
+ /**
+ * This viewer uses its own actions that delegate to the ones given
+ * by the {@link LayoutCanvas}. All the processing is actually handled
+ * directly by the canvas and this viewer only gets refreshed as a
+ * consequence of the canvas changing the XML model.
+ */
+ private void setupContextMenu() {
+
+ mMenuManager = new MenuManager();
+ mMenuManager.removeAll();
+
+ mMenuManager.add(mMoveUpAction);
+ mMenuManager.add(mMoveDownAction);
+ mMenuManager.add(new Separator());
+
+ mMenuManager.add(new SelectionManager.SelectionMenu(mGraphicalEditorPart));
+ mMenuManager.add(new Separator());
+ final String prefix = LayoutCanvas.PREFIX_CANVAS_ACTION;
+ mMenuManager.add(new DelegateAction(prefix + ActionFactory.CUT.getId()));
+ mMenuManager.add(new DelegateAction(prefix + ActionFactory.COPY.getId()));
+ mMenuManager.add(new DelegateAction(prefix + ActionFactory.PASTE.getId()));
+
+ mMenuManager.add(new Separator());
+
+ mMenuManager.add(new DelegateAction(prefix + ActionFactory.DELETE.getId()));
+
+ mMenuManager.addMenuListener(new IMenuListener() {
+ @Override
+ public void menuAboutToShow(IMenuManager manager) {
+ // Update all actions to match their LayoutCanvas counterparts
+ for (IContributionItem contrib : manager.getItems()) {
+ if (contrib instanceof ActionContributionItem) {
+ IAction action = ((ActionContributionItem) contrib).getAction();
+ if (action instanceof DelegateAction) {
+ ((DelegateAction) action).updateFromEditorPart(mGraphicalEditorPart);
+ }
+ }
+ }
+ }
+ });
+
+ new DynamicContextMenu(
+ mGraphicalEditorPart.getEditorDelegate(),
+ mGraphicalEditorPart.getCanvasControl(),
+ mMenuManager);
+
+ getTreeViewer().getTree().setMenu(mMenuManager.createContextMenu(getControl()));
+
+ // Update Move Up/Move Down state only when the menu is opened
+ getTreeViewer().getTree().addMenuDetectListener(new MenuDetectListener() {
+ @Override
+ public void menuDetected(MenuDetectEvent e) {
+ mMenuManager.update(IAction.ENABLED);
+ }
+ });
+ }
+
+ /**
+ * An action that delegates its properties and behavior to a target action.
+ * The target action can be null or it can change overtime, typically as the
+ * layout canvas' editor part is activated or closed.
+ */
+ private static class DelegateAction extends Action {
+ private IAction mTargetAction;
+ private final String mCanvasActionId;
+
+ public DelegateAction(String canvasActionId) {
+ super(canvasActionId);
+ setId(canvasActionId);
+ mCanvasActionId = canvasActionId;
+ }
+
+ // --- Methods form IAction ---
+
+ /** Returns the target action's {@link #isEnabled()} if defined, or false. */
+ @Override
+ public boolean isEnabled() {
+ return mTargetAction == null ? false : mTargetAction.isEnabled();
+ }
+
+ /** Returns the target action's {@link #isChecked()} if defined, or false. */
+ @Override
+ public boolean isChecked() {
+ return mTargetAction == null ? false : mTargetAction.isChecked();
+ }
+
+ /** Returns the target action's {@link #isHandled()} if defined, or false. */
+ @Override
+ public boolean isHandled() {
+ return mTargetAction == null ? false : mTargetAction.isHandled();
+ }
+
+ /** Runs the target action if defined. */
+ @Override
+ public void run() {
+ if (mTargetAction != null) {
+ mTargetAction.run();
+ }
+ super.run();
+ }
+
+ /**
+ * Updates this action to delegate to its counterpart in the given editor part
+ *
+ * @param editorPart The editor being updated
+ */
+ public void updateFromEditorPart(GraphicalEditorPart editorPart) {
+ LayoutCanvas canvas = editorPart == null ? null : editorPart.getCanvasControl();
+ if (canvas == null) {
+ mTargetAction = null;
+ } else {
+ mTargetAction = canvas.getAction(mCanvasActionId);
+ }
+
+ if (mTargetAction != null) {
+ setText(mTargetAction.getText());
+ setId(mTargetAction.getId());
+ setDescription(mTargetAction.getDescription());
+ setImageDescriptor(mTargetAction.getImageDescriptor());
+ setHoverImageDescriptor(mTargetAction.getHoverImageDescriptor());
+ setDisabledImageDescriptor(mTargetAction.getDisabledImageDescriptor());
+ setToolTipText(mTargetAction.getToolTipText());
+ setActionDefinitionId(mTargetAction.getActionDefinitionId());
+ setHelpListener(mTargetAction.getHelpListener());
+ setAccelerator(mTargetAction.getAccelerator());
+ setChecked(mTargetAction.isChecked());
+ setEnabled(mTargetAction.isEnabled());
+ } else {
+ setEnabled(false);
+ }
+ }
+ }
+
+ /** Returns the associated editor with this outline */
+ /* package */GraphicalEditorPart getEditor() {
+ return mGraphicalEditorPart;
+ }
+
+ @Override
+ public void setActionBars(IActionBars actionBars) {
+ super.setActionBars(actionBars);
+
+ // Map Outline actions to canvas actions such that they share Undo context etc
+ LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
+ canvas.updateGlobalActions(actionBars);
+
+ // Special handling for Select All since it's different than the canvas (will
+ // include selecting the root etc)
+ actionBars.setGlobalActionHandler(mTreeSelectAllAction.getId(), mTreeSelectAllAction);
+ actionBars.updateActionBars();
+ }
+
+ // ---- Move Up/Down Support ----
+
+ /** Returns true if the current selected item can be moved */
+ private boolean canMove(boolean forward) {
+ CanvasViewInfo viewInfo = getSingleSelectedItem();
+ if (viewInfo != null) {
+ UiViewElementNode node = viewInfo.getUiViewNode();
+ if (forward) {
+ return findNext(node) != null;
+ } else {
+ return findPrevious(node) != null;
+ }
+ }
+
+ return false;
+ }
+
+ /** Moves the current selected item down (forward) or up (not forward) */
+ private void move(boolean forward) {
+ CanvasViewInfo viewInfo = getSingleSelectedItem();
+ if (viewInfo != null) {
+ final Pair<UiViewElementNode, Integer> target;
+ UiViewElementNode selected = viewInfo.getUiViewNode();
+ if (forward) {
+ target = findNext(selected);
+ } else {
+ target = findPrevious(selected);
+ }
+ if (target != null) {
+ final LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
+ final SelectionManager selectionManager = canvas.getSelectionManager();
+ final ArrayList<SelectionItem> dragSelection = new ArrayList<SelectionItem>();
+ dragSelection.add(selectionManager.createSelection(viewInfo));
+ SelectionManager.sanitize(dragSelection);
+
+ if (!dragSelection.isEmpty()) {
+ final SimpleElement[] elements = SelectionItem.getAsElements(dragSelection);
+ UiViewElementNode parentNode = target.getFirst();
+ final NodeProxy targetNode = canvas.getNodeFactory().create(parentNode);
+
+ // Record children of the target right before the drop (such that we
+ // can find out after the drop which exact children were inserted)
+ Set<INode> children = new HashSet<INode>();
+ for (INode node : targetNode.getChildren()) {
+ children.add(node);
+ }
+
+ String label = MoveGesture.computeUndoLabel(targetNode,
+ elements, DND.DROP_MOVE);
+ canvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(label, new Runnable() {
+ @Override
+ public void run() {
+ InsertType insertType = InsertType.MOVE_INTO;
+ if (dragSelection.get(0).getNode().getParent() == targetNode) {
+ insertType = InsertType.MOVE_WITHIN;
+ }
+ canvas.getRulesEngine().setInsertType(insertType);
+ int index = target.getSecond();
+ BaseLayoutRule.insertAt(targetNode, elements, false, index);
+ targetNode.applyPendingChanges();
+ canvas.getClipboardSupport().deleteSelection("Remove", dragSelection);
+ }
+ });
+
+ // Now find out which nodes were added, and look up their
+ // corresponding CanvasViewInfos
+ final List<INode> added = new ArrayList<INode>();
+ for (INode node : targetNode.getChildren()) {
+ if (!children.contains(node)) {
+ added.add(node);
+ }
+ }
+
+ selectionManager.setOutlineSelection(added);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link CanvasViewInfo} for the currently selected item, or null if
+ * there are no or multiple selected items
+ *
+ * @return the current selected item if there is exactly one item selected
+ */
+ private CanvasViewInfo getSingleSelectedItem() {
+ TreeItem[] selection = getTreeViewer().getTree().getSelection();
+ if (selection.length == 1) {
+ return getViewInfo(selection[0].getData());
+ }
+
+ return null;
+ }
+
+
+ /** Returns the pair [parent,index] of the next node (when iterating forward) */
+ @VisibleForTesting
+ /* package */ static Pair<UiViewElementNode, Integer> findNext(UiViewElementNode node) {
+ UiElementNode parent = node.getUiParent();
+ if (parent == null) {
+ return null;
+ }
+
+ UiElementNode next = node.getUiNextSibling();
+ if (next != null) {
+ if (DescriptorsUtils.canInsertChildren(next.getDescriptor(), null)) {
+ return getFirstPosition(next);
+ } else {
+ return getPositionAfter(next);
+ }
+ }
+
+ next = parent.getUiNextSibling();
+ if (next != null) {
+ return getPositionBefore(next);
+ } else {
+ UiElementNode grandParent = parent.getUiParent();
+ if (grandParent != null) {
+ return getLastPosition(grandParent);
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the pair [parent,index] of the previous node (when iterating backward) */
+ @VisibleForTesting
+ /* package */ static Pair<UiViewElementNode, Integer> findPrevious(UiViewElementNode node) {
+ UiElementNode prev = node.getUiPreviousSibling();
+ if (prev != null) {
+ UiElementNode curr = prev;
+ while (true) {
+ List<UiElementNode> children = curr.getUiChildren();
+ if (children.size() > 0) {
+ curr = children.get(children.size() - 1);
+ continue;
+ }
+ if (DescriptorsUtils.canInsertChildren(curr.getDescriptor(), null)) {
+ return getFirstPosition(curr);
+ } else {
+ if (curr == prev) {
+ return getPositionBefore(curr);
+ } else {
+ return getPositionAfter(curr);
+ }
+ }
+ }
+ }
+
+ return getPositionBefore(node.getUiParent());
+ }
+
+ /** Returns the pair [parent,index] of the position immediately before the given node */
+ private static Pair<UiViewElementNode, Integer> getPositionBefore(UiElementNode node) {
+ if (node != null) {
+ UiElementNode parent = node.getUiParent();
+ if (parent != null && parent instanceof UiViewElementNode) {
+ return Pair.of((UiViewElementNode) parent, node.getUiSiblingIndex());
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the pair [parent,index] of the position immediately following the given node */
+ private static Pair<UiViewElementNode, Integer> getPositionAfter(UiElementNode node) {
+ if (node != null) {
+ UiElementNode parent = node.getUiParent();
+ if (parent != null && parent instanceof UiViewElementNode) {
+ return Pair.of((UiViewElementNode) parent, node.getUiSiblingIndex() + 1);
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the pair [parent,index] of the first position inside the given parent */
+ private static Pair<UiViewElementNode, Integer> getFirstPosition(UiElementNode parent) {
+ if (parent != null && parent instanceof UiViewElementNode) {
+ return Pair.of((UiViewElementNode) parent, 0);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the pair [parent,index] of the last position after the given node's
+ * children
+ */
+ private static Pair<UiViewElementNode, Integer> getLastPosition(UiElementNode parent) {
+ if (parent != null && parent instanceof UiViewElementNode) {
+ return Pair.of((UiViewElementNode) parent, parent.getUiChildren().size());
+ }
+
+ return null;
+ }
+
+ /**
+ * Truncates the given text such that it will fit into the given {@link StyledString}
+ * up to a maximum length of {@link #LABEL_MAX_WIDTH}.
+ *
+ * @param text the text to truncate
+ * @param string the existing string to be appended to
+ * @return the truncated string
+ */
+ private static String truncate(String text, StyledString string) {
+ int existingLength = string.length();
+
+ if (text.length() + existingLength > LABEL_MAX_WIDTH) {
+ int truncatedLength = LABEL_MAX_WIDTH - existingLength - 3;
+ if (truncatedLength > 0) {
+ return String.format("%1$s...", text.substring(0, truncatedLength));
+ } else {
+ return ""; //$NON-NLS-1$
+ }
+ }
+
+ return text;
+ }
+
+ @Override
+ public void setToolBar(IToolBarManager toolBarManager) {
+ makeContributions(null, toolBarManager, null);
+ toolBarManager.update(false);
+ }
+
+ /**
+ * Sets up a custom tooltip when hovering over tree items. It currently displays the error
+ * message for the lint warning associated with each node, if any (and only if the hover
+ * is over the icon portion).
+ */
+ private void setupTooltip() {
+ final Tree tree = getTreeViewer().getTree();
+
+ // This is based on SWT Snippet 125
+ final Listener listener = new Listener() {
+ Shell mTip = null;
+ Label mLabel = null;
+
+ @Override
+ public void handleEvent(Event event) {
+ switch(event.type) {
+ case SWT.Dispose:
+ case SWT.KeyDown:
+ case SWT.MouseExit:
+ case SWT.MouseDown:
+ case SWT.MouseMove:
+ if (mTip != null) {
+ mTip.dispose();
+ mTip = null;
+ mLabel = null;
+ }
+ break;
+ case SWT.MouseHover:
+ if (mTip != null) {
+ mTip.dispose();
+ mTip = null;
+ mLabel = null;
+ }
+
+ String tooltip = null;
+
+ TreeItem item = tree.getItem(new Point(event.x, event.y));
+ if (item != null) {
+ Rectangle rect = item.getBounds(0);
+ if (event.x - rect.x > 16) { // 16: Standard width of our outline icons
+ return;
+ }
+
+ Object data = item.getData();
+ if (data != null && data instanceof CanvasViewInfo) {
+ LayoutEditorDelegate editor = mGraphicalEditorPart.getEditorDelegate();
+ CanvasViewInfo vi = (CanvasViewInfo) data;
+ IMarker marker = editor.getIssueForNode(vi.getUiViewNode());
+ if (marker != null) {
+ tooltip = marker.getAttribute(IMarker.MESSAGE, null);
+ }
+ }
+
+ if (tooltip != null) {
+ Shell shell = tree.getShell();
+ Display display = tree.getDisplay();
+
+ Color fg = display.getSystemColor(SWT.COLOR_INFO_FOREGROUND);
+ Color bg = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND);
+ mTip = new Shell(shell, SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL);
+ mTip.setBackground(bg);
+ FillLayout layout = new FillLayout();
+ layout.marginWidth = 1;
+ layout.marginHeight = 1;
+ mTip.setLayout(layout);
+ mLabel = new Label(mTip, SWT.WRAP);
+ mLabel.setForeground(fg);
+ mLabel.setBackground(bg);
+ mLabel.setText(tooltip);
+ mLabel.addListener(SWT.MouseExit, this);
+ mLabel.addListener(SWT.MouseDown, this);
+
+ Point pt = tree.toDisplay(rect.x, rect.y + rect.height);
+ Rectangle displayBounds = display.getBounds();
+ // -10: Don't extend -all- the way to the edge of the screen
+ // which would make it look like it has been cropped
+ int availableWidth = displayBounds.x + displayBounds.width - pt.x - 10;
+ if (availableWidth < 80) {
+ availableWidth = 80;
+ }
+ Point size = mTip.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+ if (size.x > availableWidth) {
+ size = mTip.computeSize(availableWidth, SWT.DEFAULT);
+ }
+ mTip.setBounds(pt.x, pt.y, size.x, size.y);
+
+ mTip.setVisible(true);
+ }
+ }
+ }
+ }
+ };
+
+ tree.addListener(SWT.Dispose, listener);
+ tree.addListener(SWT.KeyDown, listener);
+ tree.addListener(SWT.MouseMove, listener);
+ tree.addListener(SWT.MouseHover, listener);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Overlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Overlay.java
new file mode 100644
index 000000000..9b7e0eb18
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Overlay.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2010 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 org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+
+/**
+ * An Overlay is a set of graphics which can be painted on top of the visual
+ * editor. Different {@link Gesture}s produce context specific overlays, such as
+ * swiping rectangles from the {@link MarqueeGesture} and guidelines from the
+ * {@link MoveGesture}.
+ */
+public abstract class Overlay {
+ private Device mDevice;
+
+ /** Whether the hover is hidden */
+ private boolean mHiding;
+
+ /**
+ * Construct the overlay, using the given graphics context for painting.
+ */
+ public Overlay() {
+ super();
+ }
+
+ /**
+ * Initializes the overlay before the first use, if applicable. This is a
+ * good place to initialize resources like colors.
+ *
+ * @param device The device to allocate resources for; the parameter passed
+ * to {@link #paint} will correspond to this device.
+ */
+ public void create(Device device) {
+ mDevice = device;
+ }
+
+ /**
+ * Releases resources held by the overlay. Called by the editor when an
+ * overlay has been removed.
+ */
+ public void dispose() {
+ }
+
+ /**
+ * Paints the overlay.
+ *
+ * @param gc The SWT {@link GC} object to draw into.
+ */
+ public void paint(GC gc) {
+ throw new IllegalArgumentException("paint() not implemented, probably done "
+ + "with specialized paint signature");
+ }
+
+ /** Returns the device associated with this overlay */
+ public Device getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Returns whether the overlay is hidden
+ *
+ * @return true if the selection overlay is hidden
+ */
+ public boolean isHiding() {
+ return mHiding;
+ }
+
+ /**
+ * Hides the overlay
+ *
+ * @param hiding true to hide the overlay, false to unhide it (default)
+ */
+ public void setHiding(boolean hiding) {
+ mHiding = hiding;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java
new file mode 100644
index 000000000..46168b70f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java
@@ -0,0 +1,1265 @@
+/*
+ * 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 static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_TEXT;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+import static com.android.SdkConstants.XMLNS_ANDROID;
+import static com.android.SdkConstants.XMLNS_URI;
+
+import com.android.ide.common.api.InsertType;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.RuleAction.Toggle;
+import com.android.ide.common.rendering.LayoutLibrary;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+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.descriptors.CustomViewDescriptorService;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.PaletteMetadataDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository.RenderMode;
+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.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+import com.android.utils.Pair;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IToolBarManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DragSource;
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.MenuDetectEvent;
+import org.eclipse.swt.events.MenuDetectListener;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+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.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+import org.eclipse.wb.internal.core.editor.structure.IPage;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A palette control for the {@link GraphicalEditorPart}.
+ * <p/>
+ * The palette contains several groups, each with a UI name (e.g. layouts and views) and each
+ * with a list of element descriptors.
+ * <p/>
+ *
+ * TODO list:
+ * - The available items should depend on the actual GLE2 Canvas selection. Selected android
+ * views should force filtering on what they accept can be dropped on them (e.g. TabHost,
+ * TableLayout). Should enable/disable them, not hide them, to avoid shuffling around.
+ * - Optional: a text filter
+ * - Optional: have context-sensitive tools items, e.g. selection arrow tool,
+ * group selection tool, alignment, etc.
+ */
+public class PaletteControl extends Composite {
+
+ /**
+ * Wrapper to create a {@link PaletteControl}
+ */
+ static class PalettePage implements IPage {
+ private final GraphicalEditorPart mEditorPart;
+ private PaletteControl mControl;
+
+ PalettePage(GraphicalEditorPart editor) {
+ mEditorPart = editor;
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ mControl = new PaletteControl(parent, mEditorPart);
+ }
+
+ @Override
+ public Control getControl() {
+ return mControl;
+ }
+
+ @Override
+ public void dispose() {
+ mControl.dispose();
+ }
+
+ @Override
+ public void setToolBar(IToolBarManager toolBarManager) {
+ }
+
+ /**
+ * Add tool bar items to the given toolbar
+ *
+ * @param toolbar the toolbar to add items into
+ */
+ void createToolbarItems(final ToolBar toolbar) {
+ final ToolItem popupMenuItem = new ToolItem(toolbar, SWT.PUSH);
+ popupMenuItem.setToolTipText("View Menu");
+ popupMenuItem.setImage(IconFactory.getInstance().getIcon("view_menu"));
+ popupMenuItem.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Rectangle bounds = popupMenuItem.getBounds();
+ // Align menu horizontally with the toolbar button and
+ // vertically with the bottom of the toolbar
+ Point point = toolbar.toDisplay(bounds.x, bounds.y + bounds.height);
+ mControl.showMenu(point.x, point.y);
+ }
+ });
+ }
+
+ @Override
+ public void setFocus() {
+ mControl.setFocus();
+ }
+ }
+
+ /**
+ * The parent grid layout that contains all the {@link Toggle} and
+ * {@link IconTextItem} widgets.
+ */
+ private GraphicalEditorPart mEditor;
+ private Color mBackground;
+ private Color mForeground;
+
+ /** The palette modes control various ways to visualize and lay out the views */
+ private static enum PaletteMode {
+ /** Show rendered previews of the views */
+ PREVIEW("Show Previews", true),
+ /** Show rendered previews of the views, scaled down to 75% */
+ SMALL_PREVIEW("Show Small Previews", true),
+ /** Show rendered previews of the views, scaled down to 50% */
+ TINY_PREVIEW("Show Tiny Previews", true),
+ /** Show an icon + text label */
+ ICON_TEXT("Show Icon and Text", false),
+ /** Show only icons, packed multiple per row */
+ ICON_ONLY("Show Only Icons", true);
+
+ PaletteMode(String actionLabel, boolean wrap) {
+ mActionLabel = actionLabel;
+ mWrap = wrap;
+ }
+
+ public String getActionLabel() {
+ return mActionLabel;
+ }
+
+ public boolean getWrap() {
+ return mWrap;
+ }
+
+ public boolean isPreview() {
+ return this == PREVIEW || this == SMALL_PREVIEW || this == TINY_PREVIEW;
+ }
+
+ public boolean isScaledPreview() {
+ return this == SMALL_PREVIEW || this == TINY_PREVIEW;
+ }
+
+ private final String mActionLabel;
+ private final boolean mWrap;
+ };
+
+ /** Token used in preference string to record alphabetical sorting */
+ private static final String VALUE_ALPHABETICAL = "alpha"; //$NON-NLS-1$
+ /** Token used in preference string to record categories being turned off */
+ private static final String VALUE_NO_CATEGORIES = "nocat"; //$NON-NLS-1$
+ /** Token used in preference string to record auto close being turned off */
+ private static final String VALUE_NO_AUTOCLOSE = "noauto"; //$NON-NLS-1$
+
+ private final PreviewIconFactory mPreviewIconFactory = new PreviewIconFactory(this);
+ private PaletteMode mPaletteMode = null;
+ /** Use alphabetical sorting instead of natural order? */
+ private boolean mAlphabetical;
+ /** Use categories instead of a single large list of views? */
+ private boolean mCategories = true;
+ /** Auto-close the previous category when new categories are opened */
+ private boolean mAutoClose = true;
+ private AccordionControl mAccordion;
+ private String mCurrentTheme;
+ private String mCurrentDevice;
+ private IAndroidTarget mCurrentTarget;
+ private AndroidTargetData mCurrentTargetData;
+
+ /**
+ * Create the composite.
+ * @param parent The parent composite.
+ * @param editor An editor associated with this palette.
+ */
+ public PaletteControl(Composite parent, GraphicalEditorPart editor) {
+ super(parent, SWT.NONE);
+
+ mEditor = editor;
+ }
+
+ /** Reads UI mode from persistent store to preserve palette mode across IDE sessions */
+ private void loadPaletteMode() {
+ String paletteModes = AdtPrefs.getPrefs().getPaletteModes();
+ if (paletteModes.length() > 0) {
+ String[] tokens = paletteModes.split(","); //$NON-NLS-1$
+ try {
+ mPaletteMode = PaletteMode.valueOf(tokens[0]);
+ } catch (Throwable t) {
+ mPaletteMode = PaletteMode.values()[0];
+ }
+ mAlphabetical = paletteModes.contains(VALUE_ALPHABETICAL);
+ mCategories = !paletteModes.contains(VALUE_NO_CATEGORIES);
+ mAutoClose = !paletteModes.contains(VALUE_NO_AUTOCLOSE);
+ } else {
+ mPaletteMode = PaletteMode.SMALL_PREVIEW;
+ }
+ }
+
+ /**
+ * Returns the most recently stored version of auto-close-mode; this is the last
+ * user-initiated setting of the auto-close mode (we programmatically switch modes when
+ * you enter icons-only mode, and set it back to this when going to any other mode)
+ */
+ private boolean getSavedAutoCloseMode() {
+ return !AdtPrefs.getPrefs().getPaletteModes().contains(VALUE_NO_AUTOCLOSE);
+ }
+
+ /** Saves UI mode to persistent store to preserve palette mode across IDE sessions */
+ private void savePaletteMode() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(mPaletteMode);
+ if (mAlphabetical) {
+ sb.append(',').append(VALUE_ALPHABETICAL);
+ }
+ if (!mCategories) {
+ sb.append(',').append(VALUE_NO_CATEGORIES);
+ }
+ if (!mAutoClose) {
+ sb.append(',').append(VALUE_NO_AUTOCLOSE);
+ }
+ AdtPrefs.getPrefs().setPaletteModes(sb.toString());
+ }
+
+ private void refreshPalette() {
+ IAndroidTarget oldTarget = mCurrentTarget;
+ mCurrentTarget = null;
+ mCurrentTargetData = null;
+ mCurrentTheme = null;
+ mCurrentDevice = null;
+ reloadPalette(oldTarget);
+ }
+
+ @Override
+ protected void checkSubclass() {
+ // Disable the check that prevents subclassing of SWT components
+ }
+
+ @Override
+ public void dispose() {
+ if (mBackground != null) {
+ mBackground.dispose();
+ mBackground = null;
+ }
+ if (mForeground != null) {
+ mForeground.dispose();
+ mForeground = null;
+ }
+
+ super.dispose();
+ }
+
+ /**
+ * Returns the currently displayed target
+ *
+ * @return the current target, or null
+ */
+ public IAndroidTarget getCurrentTarget() {
+ return mCurrentTarget;
+ }
+
+ /**
+ * Returns the currently displayed theme (in palette modes that support previewing)
+ *
+ * @return the current theme, or null
+ */
+ public String getCurrentTheme() {
+ return mCurrentTheme;
+ }
+
+ /**
+ * Returns the currently displayed device (in palette modes that support previewing)
+ *
+ * @return the current device, or null
+ */
+ public String getCurrentDevice() {
+ return mCurrentDevice;
+ }
+
+ /** Returns true if previews in the palette should be made available */
+ private boolean previewsAvailable() {
+ // Not layoutlib 5 -- we require custom background support to do
+ // a decent job with previews
+ LayoutLibrary layoutLibrary = mEditor.getLayoutLibrary();
+ return layoutLibrary != null && layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR);
+ }
+
+ /**
+ * Loads or reloads the palette elements by using the layout and view descriptors from the
+ * given target data.
+ *
+ * @param target The target that has just been loaded
+ */
+ public void reloadPalette(IAndroidTarget target) {
+ ConfigurationChooser configChooser = mEditor.getConfigurationChooser();
+ String theme = configChooser.getThemeName();
+ String device = configChooser.getDeviceName();
+ if (device == null) {
+ return;
+ }
+ AndroidTargetData targetData =
+ target != null ? Sdk.getCurrent().getTargetData(target) : null;
+ if (target == mCurrentTarget && targetData == mCurrentTargetData
+ && mCurrentTheme != null && mCurrentTheme.equals(theme)
+ && mCurrentDevice != null && mCurrentDevice.equals(device)) {
+ return;
+ }
+ mCurrentTheme = theme;
+ mCurrentTarget = target;
+ mCurrentTargetData = targetData;
+ mCurrentDevice = device;
+ mPreviewIconFactory.reset();
+
+ if (targetData == null) {
+ return;
+ }
+
+ Set<String> expandedCategories = null;
+ if (mAccordion != null) {
+ expandedCategories = mAccordion.getExpandedCategories();
+ // We auto-expand all categories when showing icons-only. When returning to some
+ // other mode we don't want to retain all categories open.
+ if (expandedCategories.size() > 3) {
+ expandedCategories = null;
+ }
+ }
+
+ // Erase old content and recreate new
+ for (Control c : getChildren()) {
+ c.dispose();
+ }
+
+ if (mPaletteMode == null) {
+ loadPaletteMode();
+ assert mPaletteMode != null;
+ }
+
+ // Ensure that the palette mode is supported on this version of the layout library
+ if (!previewsAvailable()) {
+ if (mPaletteMode.isPreview()) {
+ mPaletteMode = PaletteMode.ICON_TEXT;
+ }
+ }
+
+ if (mPaletteMode.isPreview()) {
+ if (mForeground != null) {
+ mForeground.dispose();
+ mForeground = null;
+ }
+ if (mBackground != null) {
+ mBackground.dispose();
+ mBackground = null;
+ }
+ RGB background = mPreviewIconFactory.getBackgroundColor();
+ if (background != null) {
+ mBackground = new Color(getDisplay(), background);
+ }
+ RGB foreground = mPreviewIconFactory.getForegroundColor();
+ if (foreground != null) {
+ mForeground = new Color(getDisplay(), foreground);
+ }
+ }
+
+ List<String> headers = Collections.emptyList();
+ final Map<String, List<ViewElementDescriptor>> categoryToItems;
+ categoryToItems = new HashMap<String, List<ViewElementDescriptor>>();
+ headers = new ArrayList<String>();
+ List<Pair<String,List<ViewElementDescriptor>>> paletteEntries =
+ ViewMetadataRepository.get().getPaletteEntries(targetData,
+ mAlphabetical, mCategories);
+ for (Pair<String,List<ViewElementDescriptor>> pair : paletteEntries) {
+ String category = pair.getFirst();
+ List<ViewElementDescriptor> categoryItems = pair.getSecond();
+ headers.add(category);
+ categoryToItems.put(category, categoryItems);
+ }
+
+ headers.add("Custom & Library Views");
+
+ // Set the categories to expand the first item if
+ // (1) we don't have a previously selected category, or
+ // (2) there's just one category anyway, or
+ // (3) the set of categories have changed so our previously selected category
+ // doesn't exist anymore (can happen when you toggle "Show Categories")
+ if ((expandedCategories == null && headers.size() > 0) || headers.size() == 1 ||
+ (expandedCategories != null && expandedCategories.size() >= 1
+ && !headers.contains(
+ expandedCategories.iterator().next().replace("&&", "&")))) { //$NON-NLS-1$ //$NON-NLS-2$
+ // Expand the first category if we don't have a previous selection (e.g. refresh)
+ expandedCategories = Collections.singleton(headers.get(0));
+ }
+
+ boolean wrap = mPaletteMode.getWrap();
+
+ // Pack icon-only view vertically; others stretch to fill palette region
+ boolean fillVertical = mPaletteMode != PaletteMode.ICON_ONLY;
+
+ mAccordion = new AccordionControl(this, SWT.NONE, headers, fillVertical, wrap,
+ expandedCategories) {
+ @Override
+ protected Composite createChildContainer(Composite parent, Object header, int style) {
+ assert categoryToItems != null;
+ List<ViewElementDescriptor> list = categoryToItems.get(header);
+ final Composite composite;
+ if (list == null) {
+ assert header.equals("Custom & Library Views");
+
+ Composite wrapper = new Composite(parent, SWT.NONE);
+ GridLayout gridLayout = new GridLayout(1, false);
+ gridLayout.marginWidth = gridLayout.marginHeight = 0;
+ gridLayout.horizontalSpacing = gridLayout.verticalSpacing = 0;
+ gridLayout.marginBottom = 3;
+ wrapper.setLayout(gridLayout);
+ if (mPaletteMode.isPreview() && mBackground != null) {
+ wrapper.setBackground(mBackground);
+ }
+ composite = super.createChildContainer(wrapper, header, style);
+ if (mPaletteMode.isPreview() && mBackground != null) {
+ composite.setBackground(mBackground);
+ }
+ composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
+
+ Button refreshButton = new Button(wrapper, SWT.PUSH | SWT.FLAT);
+ refreshButton.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER,
+ false, false, 1, 1));
+ refreshButton.setText("Refresh");
+ refreshButton.setImage(IconFactory.getInstance().getIcon("refresh")); //$NON-NLS-1$
+ refreshButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ CustomViewFinder finder = CustomViewFinder.get(mEditor.getProject());
+ finder.refresh(new ViewFinderListener(composite));
+ }
+ });
+
+ wrapper.layout(true);
+ } else {
+ composite = super.createChildContainer(parent, header, style);
+ if (mPaletteMode.isPreview() && mBackground != null) {
+ composite.setBackground(mBackground);
+ }
+ }
+ addMenu(composite);
+ return composite;
+ }
+ @Override
+ protected void createChildren(Composite parent, Object header) {
+ assert categoryToItems != null;
+ List<ViewElementDescriptor> list = categoryToItems.get(header);
+ if (list == null) {
+ assert header.equals("Custom & Library Views");
+ addCustomItems(parent);
+ return;
+ } else {
+ for (ViewElementDescriptor desc : list) {
+ createItem(parent, desc);
+ }
+ }
+ }
+ };
+ addMenu(mAccordion);
+ for (CLabel headerLabel : mAccordion.getHeaderLabels()) {
+ addMenu(headerLabel);
+ }
+ setLayout(new FillLayout());
+
+ // Expand All for icon-only mode, but don't store it as the persistent auto-close mode;
+ // when we enter other modes it will read back whatever persistent mode.
+ if (mPaletteMode == PaletteMode.ICON_ONLY) {
+ mAccordion.expandAll(true);
+ mAccordion.setAutoClose(false);
+ } else {
+ mAccordion.setAutoClose(getSavedAutoCloseMode());
+ }
+
+ layout(true);
+ }
+
+ protected void addCustomItems(final Composite parent) {
+ final CustomViewFinder finder = CustomViewFinder.get(mEditor.getProject());
+ Collection<String> allViews = finder.getAllViews();
+ if (allViews == null) { // Not yet initialized: trigger an async refresh
+ finder.refresh(new ViewFinderListener(parent));
+ return;
+ }
+
+ // Remove previous content
+ for (Control c : parent.getChildren()) {
+ c.dispose();
+ }
+
+ // Add new views
+ for (final String fqcn : allViews) {
+ CustomViewDescriptorService service = CustomViewDescriptorService.getInstance();
+ ViewElementDescriptor desc = service.getDescriptor(mEditor.getProject(), fqcn);
+ if (desc == null) {
+ // The descriptor lookup performs validation steps of the class, and may
+ // in some cases determine that this is not a view and will return null;
+ // guard against that.
+ continue;
+ }
+
+ Control item = createItem(parent, desc);
+
+ // Add control-click listener on custom view items to you can warp to
+ // (and double click listener too -- the more discoverable, the better.)
+ if (item instanceof IconTextItem) {
+ IconTextItem it = (IconTextItem) item;
+ it.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseDoubleClick(MouseEvent e) {
+ AdtPlugin.openJavaClass(mEditor.getProject(), fqcn);
+ }
+
+ @Override
+ public void mouseDown(MouseEvent e) {
+ if ((e.stateMask & SWT.MOD1) != 0) {
+ AdtPlugin.openJavaClass(mEditor.getProject(), fqcn);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ /* package */ GraphicalEditorPart getEditor() {
+ return mEditor;
+ }
+
+ private Control createItem(Composite parent, ViewElementDescriptor desc) {
+ Control item = null;
+ switch (mPaletteMode) {
+ case SMALL_PREVIEW:
+ case TINY_PREVIEW:
+ case PREVIEW: {
+ ImageDescriptor descriptor = mPreviewIconFactory.getImageDescriptor(desc);
+ if (descriptor != null) {
+ Image image = descriptor.createImage();
+ ImageControl imageControl = new ImageControl(parent, SWT.None, image);
+ if (mPaletteMode.isScaledPreview()) {
+ // Try to preserve the overall size since rendering sizes typically
+ // vary with the dpi - so while the scaling factor for a 160 dpi
+ // rendering the scaling factor should be 0.5, for a 320 dpi one the
+ // scaling factor should be half that, 0.25.
+ float scale = 1.0f;
+ if (mPaletteMode == PaletteMode.SMALL_PREVIEW) {
+ scale = 0.75f;
+ } else if (mPaletteMode == PaletteMode.TINY_PREVIEW) {
+ scale = 0.5f;
+ }
+ ConfigurationChooser chooser = mEditor.getConfigurationChooser();
+ int dpi = chooser.getConfiguration().getDensity().getDpiValue();
+ while (dpi > 160) {
+ scale = scale / 2;
+ dpi = dpi / 2;
+ }
+ imageControl.setScale(scale);
+ }
+ imageControl.setHoverColor(getDisplay().getSystemColor(SWT.COLOR_WHITE));
+ if (mBackground != null) {
+ imageControl.setBackground(mBackground);
+ }
+ String toolTip = desc.getUiName();
+ // It appears pretty much none of the descriptors have tooltips
+ //String descToolTip = desc.getTooltip();
+ //if (descToolTip != null && descToolTip.length() > 0) {
+ // toolTip = toolTip + "\n" + descToolTip;
+ //}
+ imageControl.setToolTipText(toolTip);
+
+ item = imageControl;
+ } else {
+ // Just use an Icon+Text item for these for now
+ item = new IconTextItem(parent, desc);
+ if (mForeground != null) {
+ item.setForeground(mForeground);
+ item.setBackground(mBackground);
+ }
+ }
+ break;
+ }
+ case ICON_TEXT: {
+ item = new IconTextItem(parent, desc);
+ break;
+ }
+ case ICON_ONLY: {
+ item = new ImageControl(parent, SWT.None, desc.getGenericIcon());
+ item.setToolTipText(desc.getUiName());
+ break;
+ }
+ default:
+ throw new IllegalArgumentException("Not yet implemented");
+ }
+
+ final DragSource source = new DragSource(item, DND.DROP_COPY);
+ source.setTransfer(new Transfer[] { SimpleXmlTransfer.getInstance() });
+ source.addDragListener(new DescDragSourceListener(desc));
+ item.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ source.dispose();
+ }
+ });
+ addMenu(item);
+
+ return item;
+ }
+
+ /**
+ * An Item widget represents one {@link ElementDescriptor} that can be dropped on the
+ * GLE2 canvas using drag'n'drop.
+ */
+ private static class IconTextItem extends CLabel implements MouseTrackListener {
+
+ private boolean mMouseIn;
+
+ public IconTextItem(Composite parent, ViewElementDescriptor desc) {
+ super(parent, SWT.NONE);
+ mMouseIn = false;
+
+ setText(desc.getUiName());
+ setImage(desc.getGenericIcon());
+ setToolTipText(desc.getTooltip());
+ addMouseTrackListener(this);
+ }
+
+ @Override
+ public int getStyle() {
+ int style = super.getStyle();
+ if (mMouseIn) {
+ style |= SWT.SHADOW_IN;
+ }
+ return style;
+ }
+
+ @Override
+ public void mouseEnter(MouseEvent e) {
+ if (!mMouseIn) {
+ mMouseIn = true;
+ redraw();
+ }
+ }
+
+ @Override
+ public void mouseExit(MouseEvent e) {
+ if (mMouseIn) {
+ mMouseIn = false;
+ redraw();
+ }
+ }
+
+ @Override
+ public void mouseHover(MouseEvent e) {
+ // pass
+ }
+ }
+
+ /**
+ * A {@link DragSourceListener} that deals with drag'n'drop of
+ * {@link ElementDescriptor}s.
+ */
+ private class DescDragSourceListener implements DragSourceListener {
+ private final ViewElementDescriptor mDesc;
+ private SimpleElement[] mElements;
+
+ public DescDragSourceListener(ViewElementDescriptor desc) {
+ mDesc = desc;
+ }
+
+ @Override
+ public void dragStart(DragSourceEvent e) {
+ // See if we can find out the bounds of this element from a preview image.
+ // Preview images are created before the drag source listener is notified
+ // of the started drag.
+ Rect bounds = null;
+ Rect dragBounds = null;
+
+ createDragImage(e);
+ if (mImage != null && !mIsPlaceholder) {
+ int width = mImageLayoutBounds.width;
+ int height = mImageLayoutBounds.height;
+ assert mImageLayoutBounds.x == 0;
+ assert mImageLayoutBounds.y == 0;
+ bounds = new Rect(0, 0, width, height);
+ double scale = mEditor.getCanvasControl().getScale();
+ int scaledWidth = (int) (scale * width);
+ int scaledHeight = (int) (scale * height);
+ int x = -scaledWidth / 2;
+ int y = -scaledHeight / 2;
+ dragBounds = new Rect(x, y, scaledWidth, scaledHeight);
+ }
+
+ SimpleElement se = new SimpleElement(
+ SimpleXmlTransfer.getFqcn(mDesc),
+ null /* parentFqcn */,
+ bounds /* bounds */,
+ null /* parentBounds */);
+ if (mDesc instanceof PaletteMetadataDescriptor) {
+ PaletteMetadataDescriptor pm = (PaletteMetadataDescriptor) mDesc;
+ pm.initializeNew(se);
+ }
+ mElements = new SimpleElement[] { se };
+
+ // Register this as the current dragged data
+ GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance();
+ dragInfo.startDrag(
+ mElements,
+ null /* selection */,
+ null /* canvas */,
+ null /* removeSource */);
+ dragInfo.setDragBounds(dragBounds);
+ dragInfo.setDragBaseline(mBaseline);
+
+
+ e.doit = true;
+ }
+
+ @Override
+ public void dragSetData(DragSourceEvent e) {
+ // Provide the data for the drop when requested by the other side.
+ if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) {
+ e.data = mElements;
+ }
+ }
+
+ @Override
+ public void dragFinished(DragSourceEvent e) {
+ // Unregister the dragged data.
+ GlobalCanvasDragInfo.getInstance().stopDrag();
+ mElements = null;
+ if (mImage != null) {
+ mImage.dispose();
+ mImage = null;
+ }
+ }
+
+ // TODO: Figure out the right dimensions to use for rendering.
+ // We WILL crop this after rendering, but for performance reasons it would be good
+ // not to make it much larger than necessary since to crop this we rely on
+ // actually scanning pixels.
+
+ /**
+ * Width of the rendered preview image (before it is cropped), although the actual
+ * width may be smaller (since we also take the device screen's size into account)
+ */
+ private static final int MAX_RENDER_HEIGHT = 400;
+
+ /**
+ * Height of the rendered preview image (before it is cropped), although the
+ * actual width may be smaller (since we also take the device screen's size into
+ * account)
+ */
+ private static final int MAX_RENDER_WIDTH = 500;
+
+ /** Amount of alpha to multiply into the image (divided by 256) */
+ private static final int IMG_ALPHA = 128;
+
+ /** The image shown during the drag */
+ private Image mImage;
+ /** The non-effect bounds of the drag image */
+ private Rectangle mImageLayoutBounds;
+ private int mBaseline = -1;
+
+ /**
+ * If true, the image is a preview of the view, and if not it is a "fallback"
+ * image of some sort, such as a rendering of the palette item itself
+ */
+ private boolean mIsPlaceholder;
+
+ private void createDragImage(DragSourceEvent event) {
+ mBaseline = -1;
+ Pair<Image, Rectangle> preview = renderPreview();
+ if (preview != null) {
+ mImage = preview.getFirst();
+ mImageLayoutBounds = preview.getSecond();
+ } else {
+ mImage = null;
+ mImageLayoutBounds = null;
+ }
+
+ mIsPlaceholder = mImage == null;
+ if (mIsPlaceholder) {
+ // Couldn't render preview (or the preview is a blank image, such as for
+ // example the preview of an empty layout), so instead create a placeholder
+ // image
+ // Render the palette item itself as an image
+ Control control = ((DragSource) event.widget).getControl();
+ GC gc = new GC(control);
+ Point size = control.getSize();
+ Display display = getDisplay();
+ final Image image = new Image(display, size.x, size.y);
+ gc.copyArea(image, 0, 0);
+ gc.dispose();
+
+ BufferedImage awtImage = SwtUtils.convertToAwt(image);
+ if (awtImage != null) {
+ awtImage = ImageUtils.createDropShadow(awtImage, 3 /* shadowSize */,
+ 0.7f /* shadowAlpha */, 0x000000 /* shadowRgb */);
+ mImage = SwtUtils.convertToSwt(display, awtImage, true, IMG_ALPHA);
+ } else {
+ ImageData data = image.getImageData();
+ data.alpha = IMG_ALPHA;
+
+ // Changing the ImageData -after- constructing an image on it
+ // has no effect, so we have to construct a new image. Luckily these
+ // are tiny images.
+ mImage = new Image(display, data);
+ }
+ image.dispose();
+ }
+
+ event.image = mImage;
+
+ if (!mIsPlaceholder) {
+ // Shift the drag feedback image up such that it's centered under the
+ // mouse pointer
+ double scale = mEditor.getCanvasControl().getScale();
+ event.offsetX = (int) (scale * mImageLayoutBounds.width / 2);
+ event.offsetY = (int) (scale * mImageLayoutBounds.height / 2);
+ }
+ }
+
+ /**
+ * Performs the actual rendering of the descriptor into an image and returns the
+ * image as well as the layout bounds of the image (not including drop shadow etc)
+ */
+ private Pair<Image, Rectangle> renderPreview() {
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ RenderMode renderMode = repository.getRenderMode(mDesc.getFullClassName());
+ if (renderMode == RenderMode.SKIP) {
+ return null;
+ }
+
+ // Create blank XML document
+ Document document = DomUtilities.createEmptyDocument();
+
+ // Insert our target view's XML into it as a node
+ GraphicalEditorPart editor = getEditor();
+ LayoutEditorDelegate layoutEditorDelegate = editor.getEditorDelegate();
+
+ String viewName = mDesc.getXmlLocalName();
+ Element element = document.createElement(viewName);
+
+ // Set up a proper name space
+ Attr attr = document.createAttributeNS(XMLNS_URI, XMLNS_ANDROID);
+ attr.setValue(ANDROID_URI);
+ element.getAttributes().setNamedItemNS(attr);
+
+ element.setAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
+ element.setAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
+
+ // This doesn't apply to all, but doesn't seem to cause harm and makes for a
+ // better experience with text-oriented views like buttons and texts
+ element.setAttributeNS(ANDROID_URI, ATTR_TEXT,
+ DescriptorsUtils.getBasename(mDesc.getUiName()));
+
+ // Is this a palette variation?
+ if (mDesc instanceof PaletteMetadataDescriptor) {
+ PaletteMetadataDescriptor pm = (PaletteMetadataDescriptor) mDesc;
+ pm.initializeNew(element);
+ }
+
+ document.appendChild(element);
+
+ // Construct UI model from XML
+ AndroidTargetData data = layoutEditorDelegate.getEditor().getTargetData();
+ DocumentDescriptor documentDescriptor;
+ if (data == null) {
+ documentDescriptor = new DocumentDescriptor("temp", null/*children*/);//$NON-NLS-1$
+ } else {
+ documentDescriptor = data.getLayoutDescriptors().getDescriptor();
+ }
+ UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode();
+ model.setEditor(layoutEditorDelegate.getEditor());
+ model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider());
+ model.loadFromXmlNode(document);
+
+ // Call the create-hooks such that we for example insert mandatory
+ // children into views like the DialerFilter, apply image source attributes
+ // to ImageButtons, etc.
+ LayoutCanvas canvas = editor.getCanvasControl();
+ NodeFactory nodeFactory = canvas.getNodeFactory();
+ UiElementNode parent = model.getUiRoot();
+ UiElementNode child = parent.getUiChildren().get(0);
+ if (child instanceof UiViewElementNode) {
+ UiViewElementNode childUiNode = (UiViewElementNode) child;
+ NodeProxy childNode = nodeFactory.create(childUiNode);
+
+ // Applying create hooks as part of palette render should
+ // not trigger model updates
+ layoutEditorDelegate.getEditor().setIgnoreXmlUpdate(true);
+ try {
+ canvas.getRulesEngine().callCreateHooks(layoutEditorDelegate.getEditor(),
+ null, childNode, InsertType.CREATE_PREVIEW);
+ childNode.applyPendingChanges();
+ } catch (Throwable t) {
+ AdtPlugin.log(t, "Failed calling creation hooks for widget %1$s", viewName);
+ } finally {
+ layoutEditorDelegate.getEditor().setIgnoreXmlUpdate(false);
+ }
+ }
+
+ Integer overrideBgColor = null;
+ boolean hasTransparency = false;
+ LayoutLibrary layoutLibrary = editor.getLayoutLibrary();
+ if (layoutLibrary != null &&
+ layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR)) {
+ // It doesn't matter what the background color is as long as the alpha
+ // is 0 (fully transparent). We're using red to make it more obvious if
+ // for some reason the background is painted when it shouldn't be.
+ overrideBgColor = new Integer(0x00FF0000);
+ }
+
+ RenderSession session = null;
+ try {
+ // Use at most the size of the screen for the preview render.
+ // This is important since when we fill the size of certain views (like
+ // a SeekBar), we want it to at most be the width of the screen, and for small
+ // screens the RENDER_WIDTH was wider.
+ LayoutLog silentLogger = new LayoutLog();
+
+ session = RenderService.create(editor)
+ .setModel(model)
+ .setMaxRenderSize(MAX_RENDER_WIDTH, MAX_RENDER_HEIGHT)
+ .setLog(silentLogger)
+ .setOverrideBgColor(overrideBgColor)
+ .setDecorations(false)
+ .createRenderSession();
+ } catch (Throwable t) {
+ // Previews can fail for a variety of reasons -- let's not bug
+ // the user with it
+ return null;
+ }
+
+ if (session != null) {
+ if (session.getResult().isSuccess()) {
+ BufferedImage image = session.getImage();
+ if (image != null) {
+ BufferedImage cropped;
+ Rect initialCrop = null;
+ ViewInfo viewInfo = null;
+
+ List<ViewInfo> viewInfoList = session.getRootViews();
+
+ if (viewInfoList != null && viewInfoList.size() > 0) {
+ viewInfo = viewInfoList.get(0);
+ mBaseline = viewInfo.getBaseLine();
+ }
+
+ if (viewInfo != null) {
+ int x1 = viewInfo.getLeft();
+ int x2 = viewInfo.getRight();
+ int y2 = viewInfo.getBottom();
+ int y1 = viewInfo.getTop();
+ initialCrop = new Rect(x1, y1, x2 - x1, y2 - y1);
+ }
+
+ if (hasTransparency) {
+ cropped = ImageUtils.cropBlank(image, initialCrop);
+ } else {
+ // Find out what the "background" color is such that we can properly
+ // crop it out of the image. To do this we pick out a pixel in the
+ // bottom right unpainted area. Rather than pick the one in the far
+ // bottom corner, we pick one as close to the bounds of the view as
+ // possible (but still outside of the bounds), such that we can
+ // deal with themes like the dialog theme.
+ int edgeX = image.getWidth() -1;
+ int edgeY = image.getHeight() -1;
+ if (viewInfo != null) {
+ if (viewInfo.getRight() < image.getWidth()-1) {
+ edgeX = viewInfo.getRight()+1;
+ }
+ if (viewInfo.getBottom() < image.getHeight()-1) {
+ edgeY = viewInfo.getBottom()+1;
+ }
+ }
+ int edgeColor = image.getRGB(edgeX, edgeY);
+ cropped = ImageUtils.cropColor(image, edgeColor, initialCrop);
+ }
+
+ if (cropped != null) {
+ int width = initialCrop != null ? initialCrop.w : cropped.getWidth();
+ int height = initialCrop != null ? initialCrop.h : cropped.getHeight();
+ boolean needsContrast = hasTransparency
+ && !ImageUtils.containsDarkPixels(cropped);
+ cropped = ImageUtils.createDropShadow(cropped,
+ hasTransparency ? 3 : 5 /* shadowSize */,
+ !hasTransparency ? 0.6f : needsContrast ? 0.8f : 0.7f/*alpha*/,
+ 0x000000 /* shadowRgb */);
+
+ double scale = canvas.getScale();
+ if (scale != 1L) {
+ cropped = ImageUtils.scale(cropped, scale, scale);
+ }
+
+ Display display = getDisplay();
+ int alpha = (!hasTransparency || !needsContrast) ? IMG_ALPHA : -1;
+ Image swtImage = SwtUtils.convertToSwt(display, cropped, true, alpha);
+ Rectangle imageBounds = new Rectangle(0, 0, width, height);
+ return Pair.of(swtImage, imageBounds);
+ }
+ }
+ }
+
+ session.dispose();
+ }
+
+ return null;
+ }
+
+ /**
+ * Utility method to print out the contents of the given XML document. This is
+ * really useful when working on the preview code above. I'm including all the
+ * code inside a constant false, which means the compiler will omit all the code,
+ * but I'd like to leave it in the code base and by doing it this way rather than
+ * as commented out code the code won't be accidentally broken.
+ */
+ @SuppressWarnings("all")
+ private void dumpDocument(Document document) {
+ // Diagnostics: print out the XML that we're about to render
+ if (false) { // Will be omitted by the compiler
+ org.apache.xml.serialize.OutputFormat outputFormat =
+ new org.apache.xml.serialize.OutputFormat(
+ "XML", "ISO-8859-1", true); //$NON-NLS-1$ //$NON-NLS-2$
+ outputFormat.setIndent(2);
+ outputFormat.setLineWidth(100);
+ outputFormat.setIndenting(true);
+ outputFormat.setOmitXMLDeclaration(true);
+ outputFormat.setOmitDocumentType(true);
+ StringWriter stringWriter = new StringWriter();
+ // Using FQN here to avoid having an import above, which will result
+ // in a deprecation warning, and there isn't a way to annotate a single
+ // import element with a SuppressWarnings.
+ org.apache.xml.serialize.XMLSerializer serializer =
+ new org.apache.xml.serialize.XMLSerializer(stringWriter, outputFormat);
+ serializer.setNamespaces(true);
+ try {
+ serializer.serialize(document.getDocumentElement());
+ System.out.println(stringWriter.toString());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /** Action for switching view modes via radio buttons */
+ private class PaletteModeAction extends Action {
+ private final PaletteMode mMode;
+
+ PaletteModeAction(PaletteMode mode) {
+ super(mode.getActionLabel(), IAction.AS_RADIO_BUTTON);
+ mMode = mode;
+ boolean selected = mMode == mPaletteMode;
+ setChecked(selected);
+ setEnabled(!selected);
+ }
+
+ @Override
+ public void run() {
+ if (isEnabled()) {
+ mPaletteMode = mMode;
+ refreshPalette();
+ savePaletteMode();
+ }
+ }
+ }
+
+ /** Action for toggling various checkbox view modes - categories, sorting, etc */
+ private class ToggleViewOptionAction extends Action {
+ private final int mAction;
+ final static int TOGGLE_CATEGORY = 1;
+ final static int TOGGLE_ALPHABETICAL = 2;
+ final static int TOGGLE_AUTO_CLOSE = 3;
+ final static int REFRESH = 4;
+ final static int RESET = 5;
+
+ ToggleViewOptionAction(String title, int action, boolean checked) {
+ super(title, (action == REFRESH || action == RESET) ? IAction.AS_PUSH_BUTTON
+ : IAction.AS_CHECK_BOX);
+ mAction = action;
+ if (checked) {
+ setChecked(checked);
+ }
+ }
+
+ @Override
+ public void run() {
+ switch (mAction) {
+ case TOGGLE_CATEGORY:
+ mCategories = !mCategories;
+ refreshPalette();
+ break;
+ case TOGGLE_ALPHABETICAL:
+ mAlphabetical = !mAlphabetical;
+ refreshPalette();
+ break;
+ case TOGGLE_AUTO_CLOSE:
+ mAutoClose = !mAutoClose;
+ mAccordion.setAutoClose(mAutoClose);
+ break;
+ case REFRESH:
+ mPreviewIconFactory.refresh();
+ refreshPalette();
+ break;
+ case RESET:
+ mAlphabetical = false;
+ mCategories = true;
+ mAutoClose = true;
+ mPaletteMode = PaletteMode.SMALL_PREVIEW;
+ refreshPalette();
+ break;
+ }
+ savePaletteMode();
+ }
+ }
+
+ private void addMenu(Control control) {
+ control.addMenuDetectListener(new MenuDetectListener() {
+ @Override
+ public void menuDetected(MenuDetectEvent e) {
+ showMenu(e.x, e.y);
+ }
+ });
+ }
+
+ private void showMenu(int x, int y) {
+ MenuManager manager = new MenuManager() {
+ @Override
+ public boolean isDynamic() {
+ return true;
+ }
+ };
+ boolean previews = previewsAvailable();
+ for (PaletteMode mode : PaletteMode.values()) {
+ if (mode.isPreview() && !previews) {
+ continue;
+ }
+ manager.add(new PaletteModeAction(mode));
+ }
+ if (mPaletteMode.isPreview()) {
+ manager.add(new Separator());
+ manager.add(new ToggleViewOptionAction("Refresh Previews",
+ ToggleViewOptionAction.REFRESH,
+ false));
+ }
+ manager.add(new Separator());
+ manager.add(new ToggleViewOptionAction("Show Categories",
+ ToggleViewOptionAction.TOGGLE_CATEGORY,
+ mCategories));
+ manager.add(new ToggleViewOptionAction("Sort Alphabetically",
+ ToggleViewOptionAction.TOGGLE_ALPHABETICAL,
+ mAlphabetical));
+ manager.add(new Separator());
+ manager.add(new ToggleViewOptionAction("Auto Close Previous",
+ ToggleViewOptionAction.TOGGLE_AUTO_CLOSE,
+ mAutoClose));
+ manager.add(new Separator());
+ manager.add(new ToggleViewOptionAction("Reset",
+ ToggleViewOptionAction.RESET,
+ false));
+
+ Menu menu = manager.createContextMenu(PaletteControl.this);
+ menu.setLocation(x, y);
+ menu.setVisible(true);
+ }
+
+ private final class ViewFinderListener implements CustomViewFinder.Listener {
+ private final Composite mParent;
+
+ private ViewFinderListener(Composite parent) {
+ mParent = parent;
+ }
+
+ @Override
+ public void viewsUpdated(Collection<String> customViews,
+ Collection<String> thirdPartyViews) {
+ addCustomItems(mParent);
+ mParent.layout(true);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PlayAnimationMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PlayAnimationMenu.java
new file mode 100644
index 000000000..629a42f18
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PlayAnimationMenu.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2011 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 static com.android.SdkConstants.FD_RESOURCES;
+import static com.android.SdkConstants.FD_RES_ANIMATOR;
+import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP;
+
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.rendering.api.IAnimationListener;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.Result;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard;
+import com.android.resources.ResourceType;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchWindow;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * "Play Animation" context menu which lists available animations in the project and in
+ * the framework, as well as a "Create Animation" shortcut, and allows the animation to be
+ * run on the selection
+ * <p/>
+ * TODO: Add transport controls for play/rewind/pause/loop, and (if possible) scrubbing
+ */
+public class PlayAnimationMenu extends SubmenuAction {
+ /** Associated canvas */
+ private final LayoutCanvas mCanvas;
+ /** Whether this menu is showing local animations or framework animations */
+ private boolean mFramework;
+
+ /**
+ * Creates a "Play Animation" menu
+ *
+ * @param canvas associated canvas
+ */
+ public PlayAnimationMenu(LayoutCanvas canvas) {
+ this(canvas, "Play Animation", false);
+ }
+
+ /**
+ * Creates an animation menu; this can be used either for the outer Play animation
+ * menu, or the inner frameworks-animations list
+ *
+ * @param canvas the associated canvas
+ * @param title menu item name
+ * @param framework true to show the framework animations, false for the project (and
+ * nested framework-animation-menu) animations
+ */
+ private PlayAnimationMenu(LayoutCanvas canvas, String title, boolean framework) {
+ super(title);
+ mCanvas = canvas;
+ mFramework = framework;
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+ List<SelectionItem> selection = selectionManager.getSelections();
+ if (selection.size() != 1) {
+ addDisabledMessageItem("Select exactly one widget");
+ return;
+ }
+
+ GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ if (graphicalEditor.renderingSupports(Capability.PLAY_ANIMATION)) {
+ // List of animations
+ Collection<String> animationNames = graphicalEditor.getResourceNames(mFramework,
+ ResourceType.ANIMATOR);
+ if (animationNames.size() > 0) {
+ // Sort alphabetically
+ List<String> sortedNames = new ArrayList<String>(animationNames);
+ Collections.sort(sortedNames);
+
+ for (String animation : sortedNames) {
+ String title = animation;
+ IAction action = new PlayAnimationAction(title, animation, mFramework);
+ new ActionContributionItem(action).fill(menu, -1);
+ }
+
+ new Separator().fill(menu, -1);
+ }
+
+ if (!mFramework) {
+ // Not in the framework submenu: include recent list and create new actions
+
+ // "Create New" action
+ new ActionContributionItem(new CreateAnimationAction()).fill(menu, -1);
+
+ // Framework resources submenu
+ new Separator().fill(menu, -1);
+ PlayAnimationMenu sub = new PlayAnimationMenu(mCanvas, "Android Builtin", true);
+ new ActionContributionItem(sub).fill(menu, -1);
+ }
+ } else {
+ addDisabledMessageItem(
+ "Not supported for this SDK version; try changing the Render Target");
+ }
+ }
+
+ private class PlayAnimationAction extends Action {
+ private final String mAnimationName;
+ private final boolean mIsFrameworkAnim;
+
+ public PlayAnimationAction(String title, String animationName, boolean isFrameworkAnim) {
+ super(title, IAction.AS_PUSH_BUTTON);
+ mAnimationName = animationName;
+ mIsFrameworkAnim = isFrameworkAnim;
+ }
+
+ @Override
+ public void run() {
+ SelectionManager selectionManager = mCanvas.getSelectionManager();
+ List<SelectionItem> selection = selectionManager.getSelections();
+ SelectionItem canvasSelection = selection.get(0);
+ CanvasViewInfo info = canvasSelection.getViewInfo();
+
+ Object viewObject = info.getViewObject();
+ if (viewObject != null) {
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ RenderSession session = viewHierarchy.getSession();
+ Result r = session.animate(viewObject, mAnimationName, mIsFrameworkAnim,
+ new IAnimationListener() {
+ private boolean mPendingDrawing = false;
+
+ @Override
+ public void onNewFrame(RenderSession s) {
+ SelectionOverlay selectionOverlay = mCanvas.getSelectionOverlay();
+ if (!selectionOverlay.isHiding()) {
+ selectionOverlay.setHiding(true);
+ }
+ HoverOverlay hoverOverlay = mCanvas.getHoverOverlay();
+ if (!hoverOverlay.isHiding()) {
+ hoverOverlay.setHiding(true);
+ }
+
+ ImageOverlay imageOverlay = mCanvas.getImageOverlay();
+ imageOverlay.setImage(s.getImage(), s.isAlphaChannelImage());
+ synchronized (this) {
+ if (mPendingDrawing == false) {
+ mCanvas.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (this) {
+ mPendingDrawing = false;
+ }
+ mCanvas.redraw();
+ }
+ });
+ mPendingDrawing = true;
+ }
+ }
+ }
+
+ @Override
+ public boolean isCanceled() {
+ return false;
+ }
+
+ @Override
+ public void done(Result result) {
+ SelectionOverlay selectionOverlay = mCanvas.getSelectionOverlay();
+ selectionOverlay.setHiding(false);
+ HoverOverlay hoverOverlay = mCanvas.getHoverOverlay();
+ hoverOverlay.setHiding(false);
+
+ // Must refresh view hierarchy to force objects back to
+ // their original positions in case animations have left
+ // them elsewhere
+ mCanvas.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ GraphicalEditorPart graphicalEditor = mCanvas
+ .getEditorDelegate().getGraphicalEditor();
+ graphicalEditor.recomputeLayout();
+ }
+ });
+ }
+ });
+
+ if (!r.isSuccess()) {
+ if (r.getErrorMessage() != null) {
+ AdtPlugin.log(r.getException(), r.getErrorMessage());
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Action which brings up the "Create new XML File" wizard, pre-selected with the
+ * animation category
+ */
+ private class CreateAnimationAction extends Action {
+ public CreateAnimationAction() {
+ super("Create...", IAction.AS_PUSH_BUTTON);
+ }
+
+ @Override
+ public void run() {
+ Shell parent = mCanvas.getShell();
+ NewXmlFileWizard wizard = new NewXmlFileWizard();
+ LayoutEditorDelegate editor = mCanvas.getEditorDelegate();
+ IWorkbenchWindow workbenchWindow =
+ editor.getEditor().getEditorSite().getWorkbenchWindow();
+ IWorkbench workbench = workbenchWindow.getWorkbench();
+ String animationDir = FD_RESOURCES + WS_SEP + FD_RES_ANIMATOR;
+ Pair<IProject, String> pair = Pair.of(editor.getEditor().getProject(), animationDir);
+ IStructuredSelection selection = new StructuredSelection(pair);
+ wizard.init(workbench, selection);
+ WizardDialog dialog = new WizardDialog(parent, wizard);
+ dialog.create();
+ dialog.open();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java
new file mode 100644
index 000000000..5661b2919
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java
@@ -0,0 +1,642 @@
+/*
+ * Copyright (C) 2011 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 static com.android.SdkConstants.DOT_PNG;
+import static com.android.SdkConstants.FQCN_DATE_PICKER;
+import static com.android.SdkConstants.FQCN_EXPANDABLE_LIST_VIEW;
+import static com.android.SdkConstants.FQCN_LIST_VIEW;
+import static com.android.SdkConstants.FQCN_TIME_PICKER;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.LayoutLibrary;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
+import com.android.ide.common.rendering.api.StyleResourceValue;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.common.resources.ResourceResolver;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.PaletteMetadataDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository.RenderMode;
+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.resources.ResourceHelper;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.sdklib.IAndroidTarget;
+import com.android.utils.Pair;
+
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.RGB;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.awt.image.BufferedImage;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+
+import javax.imageio.ImageIO;
+
+/**
+ * Factory which can provide preview icons for android views of a particular SDK and
+ * editor's configuration chooser
+ */
+public class PreviewIconFactory {
+ private PaletteControl mPalette;
+ private RGB mBackground;
+ private RGB mForeground;
+ private File mImageDir;
+
+ private static final String PREVIEW_INFO_FILE = "preview.properties"; //$NON-NLS-1$
+
+ public PreviewIconFactory(PaletteControl palette) {
+ mPalette = palette;
+ }
+
+ /**
+ * Resets the state in the preview icon factory such that it will re-fetch information
+ * like the theme and SDK (the icons themselves are cached in a directory across IDE
+ * session though)
+ */
+ public void reset() {
+ mImageDir = null;
+ mBackground = null;
+ mForeground = null;
+ }
+
+ /**
+ * Deletes all the persistent state for the current settings such that it will be regenerated
+ */
+ public void refresh() {
+ File imageDir = getImageDir(false);
+ if (imageDir != null && imageDir.exists()) {
+ File[] files = imageDir.listFiles();
+ for (File file : files) {
+ file.delete();
+ }
+ imageDir.delete();
+ reset();
+ }
+ }
+
+ /**
+ * Returns an image descriptor for the given element descriptor, or null if no image
+ * could be computed. The rendering parameters (SDK, theme etc) correspond to those
+ * stored in the associated palette.
+ *
+ * @param desc the element descriptor to get an image for
+ * @return an image descriptor, or null if no image could be rendered
+ */
+ public ImageDescriptor getImageDescriptor(ElementDescriptor desc) {
+ File imageDir = getImageDir(false);
+ if (!imageDir.exists()) {
+ render();
+ }
+ File file = new File(imageDir, getFileName(desc));
+ if (file.exists()) {
+ try {
+ return ImageDescriptor.createFromURL(file.toURI().toURL());
+ } catch (MalformedURLException e) {
+ AdtPlugin.log(e, "Could not create image descriptor for %s", file);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Partition the elements in the document according to their rendering preferences;
+ * elements that should be skipped are removed, elements that should be rendered alone
+ * are placed in their own list, etc
+ *
+ * @param document the document containing render fragments for the various elements
+ * @return
+ */
+ private List<List<Element>> partitionRenderElements(Document document) {
+ List<List<Element>> elements = new ArrayList<List<Element>>();
+
+ List<Element> shared = new ArrayList<Element>();
+ Element root = document.getDocumentElement();
+ elements.add(shared);
+
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+
+ NodeList children = root.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) node;
+ String fqn = repository.getFullClassName(element);
+ assert fqn.length() > 0 : element.getNodeName();
+ RenderMode renderMode = repository.getRenderMode(fqn);
+
+ // Temporary special cases
+ if (fqn.equals(FQCN_LIST_VIEW) || fqn.equals(FQCN_EXPANDABLE_LIST_VIEW)) {
+ if (!mPalette.getEditor().renderingSupports(Capability.ADAPTER_BINDING)) {
+ renderMode = RenderMode.SKIP;
+ }
+ } else if (fqn.equals(FQCN_DATE_PICKER) || fqn.equals(FQCN_TIME_PICKER)) {
+ IAndroidTarget renderingTarget = mPalette.getEditor().getRenderingTarget();
+ // In Honeycomb, these widgets only render properly in the Holo themes.
+ int apiLevel = renderingTarget.getVersion().getApiLevel();
+ if (apiLevel == 11) {
+ String themeName = mPalette.getCurrentTheme();
+ if (themeName == null || !themeName.startsWith("Theme.Holo")) { //$NON-NLS-1$
+ // Note - it's possible that the the theme is some other theme
+ // such as a user theme which inherits from Theme.Holo and that
+ // the render -would- have worked, but it's harder to detect that
+ // scenario, so we err on the side of caution and just show an
+ // icon + name for the time widgets.
+ renderMode = RenderMode.SKIP;
+ }
+ } else if (apiLevel >= 12) {
+ // Currently broken, even for Holo.
+ renderMode = RenderMode.SKIP;
+ } // apiLevel <= 10 is fine
+ }
+
+ if (renderMode == RenderMode.ALONE) {
+ elements.add(Collections.singletonList(element));
+ } else if (renderMode == RenderMode.NORMAL) {
+ shared.add(element);
+ } else {
+ assert renderMode == RenderMode.SKIP;
+ }
+ }
+ }
+
+ return elements;
+ }
+
+ /**
+ * Renders ALL the widgets and then extracts image data for each view and saves it on
+ * disk
+ */
+ private boolean render() {
+ File imageDir = getImageDir(true);
+
+ GraphicalEditorPart editor = mPalette.getEditor();
+ LayoutEditorDelegate layoutEditorDelegate = editor.getEditorDelegate();
+ LayoutLibrary layoutLibrary = editor.getLayoutLibrary();
+ Integer overrideBgColor = null;
+ if (layoutLibrary != null) {
+ if (layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR)) {
+ Pair<RGB, RGB> themeColors = getColorsFromTheme();
+ RGB bg = themeColors.getFirst();
+ RGB fg = themeColors.getSecond();
+ if (bg != null) {
+ storeBackground(imageDir, bg, fg);
+ overrideBgColor = Integer.valueOf(ImageUtils.rgbToInt(bg, 0xFF));
+ }
+ }
+ }
+
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ Document document = repository.getRenderingConfigDoc();
+
+ if (document == null) {
+ return false;
+ }
+
+ // Construct UI model from XML
+ AndroidTargetData data = layoutEditorDelegate.getEditor().getTargetData();
+ DocumentDescriptor documentDescriptor;
+ if (data == null) {
+ documentDescriptor = new DocumentDescriptor("temp", null/*children*/);//$NON-NLS-1$
+ } else {
+ documentDescriptor = data.getLayoutDescriptors().getDescriptor();
+ }
+ UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode();
+ model.setEditor(layoutEditorDelegate.getEditor());
+ model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider());
+
+ Element documentElement = document.getDocumentElement();
+ List<List<Element>> elements = partitionRenderElements(document);
+ for (List<Element> elementGroup : elements) {
+ // Replace the document elements with the current element group
+ while (documentElement.getFirstChild() != null) {
+ documentElement.removeChild(documentElement.getFirstChild());
+ }
+ for (Element element : elementGroup) {
+ documentElement.appendChild(element);
+ }
+
+ model.loadFromXmlNode(document);
+
+ RenderSession session = null;
+ NodeList childNodes = documentElement.getChildNodes();
+ try {
+ // Important to get these sizes large enough for clients that don't support
+ // RenderMode.FULL_EXPAND such as 1.6
+ int width = 200;
+ int height = childNodes.getLength() == 1 ? 400 : 1600;
+
+ session = RenderService.create(editor)
+ .setModel(model)
+ .setOverrideRenderSize(width, height)
+ .setRenderingMode(RenderingMode.FULL_EXPAND)
+ .setLog(editor.createRenderLogger("palette"))
+ .setOverrideBgColor(overrideBgColor)
+ .setDecorations(false)
+ .createRenderSession();
+ } catch (Throwable t) {
+ // If there are internal errors previewing the components just revert to plain
+ // icons and labels
+ continue;
+ }
+
+ if (session != null) {
+ if (session.getResult().isSuccess()) {
+ BufferedImage image = session.getImage();
+ if (image != null && image.getWidth() > 0 && image.getHeight() > 0) {
+
+ // Fallback for older platforms where we couldn't do background rendering
+ // at the beginning of this method
+ if (mBackground == null) {
+ Pair<RGB, RGB> themeColors = getColorsFromTheme();
+ RGB bg = themeColors.getFirst();
+ RGB fg = themeColors.getSecond();
+
+ if (bg == null) {
+ // Just use a pixel from the rendering instead.
+ int p = image.getRGB(image.getWidth() - 1, image.getHeight() - 1);
+ // However, in this case we don't trust the foreground color
+ // even if one was found in the themes; pick one that is guaranteed
+ // to contrast with the background
+ bg = ImageUtils.intToRgb(p);
+ if (ImageUtils.getBrightness(ImageUtils.rgbToInt(bg, 255)) < 128) {
+ fg = new RGB(255, 255, 255);
+ } else {
+ fg = new RGB(0, 0, 0);
+ }
+ }
+ storeBackground(imageDir, bg, fg);
+ assert mBackground != null;
+ }
+
+ List<ViewInfo> viewInfoList = session.getRootViews();
+ if (viewInfoList != null && viewInfoList.size() > 0) {
+ // We don't render previews under a <merge> so there should
+ // only be one root.
+ ViewInfo firstRoot = viewInfoList.get(0);
+ int parentX = firstRoot.getLeft();
+ int parentY = firstRoot.getTop();
+ List<ViewInfo> infos = firstRoot.getChildren();
+ for (ViewInfo info : infos) {
+ Object cookie = info.getCookie();
+ if (!(cookie instanceof UiElementNode)) {
+ continue;
+ }
+ UiElementNode node = (UiElementNode) cookie;
+ String fileName = getFileName(node);
+ File file = new File(imageDir, fileName);
+ if (file.exists()) {
+ // On Windows, perhaps we need to rename instead?
+ file.delete();
+ }
+ int x1 = parentX + info.getLeft();
+ int y1 = parentY + info.getTop();
+ int x2 = parentX + info.getRight();
+ int y2 = parentY + info.getBottom();
+ if (x1 != x2 && y1 != y2) {
+ savePreview(file, image, x1, y1, x2, y2);
+ }
+ }
+ }
+ }
+ } else {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0, n = childNodes.getLength(); i < n; i++) {
+ Node node = childNodes.item(i);
+ if (node instanceof Element) {
+ Element e = (Element) node;
+ String fqn = repository.getFullClassName(e);
+ fqn = fqn.substring(fqn.lastIndexOf('.') + 1);
+ if (sb.length() > 0) {
+ sb.append(", "); //$NON-NLS-1$
+ }
+ sb.append(fqn);
+ }
+ }
+ AdtPlugin.log(IStatus.WARNING, "Failed to render set of icons for %1$s",
+ sb.toString());
+
+ if (session.getResult().getException() != null) {
+ AdtPlugin.log(session.getResult().getException(),
+ session.getResult().getErrorMessage());
+ } else if (session.getResult().getErrorMessage() != null) {
+ AdtPlugin.log(IStatus.WARNING, session.getResult().getErrorMessage());
+ }
+ }
+
+ session.dispose();
+ }
+ }
+
+ mPalette.getEditor().recomputeLayout();
+
+ return true;
+ }
+
+ /**
+ * Look up the background and foreground colors from the theme. May not find either
+ * the background or foreground or both, but will always return a pair of possibly
+ * null colors.
+ *
+ * @return a pair of possibly null color descriptions
+ */
+ @NonNull
+ private Pair<RGB, RGB> getColorsFromTheme() {
+ RGB background = null;
+ RGB foreground = null;
+
+ ResourceResolver resources = mPalette.getEditor().getResourceResolver();
+ if (resources == null) {
+ return Pair.of(background, foreground);
+ }
+ StyleResourceValue theme = resources.getCurrentTheme();
+ if (theme != null) {
+ background = resolveThemeColor(resources, "windowBackground"); //$NON-NLS-1$
+ if (background == null) {
+ background = renderDrawableResource("windowBackground"); //$NON-NLS-1$
+ // This causes some harm with some themes: We'll find a color, say black,
+ // that isn't actually rendered in the theme. Better to use null here,
+ // which will cause the caller to pick a pixel from the observed background
+ // instead.
+ //if (background == null) {
+ // background = resolveThemeColor(resources, "colorBackground"); //$NON-NLS-1$
+ //}
+ }
+ foreground = resolveThemeColor(resources, "textColorPrimary"); //$NON-NLS-1$
+ }
+
+ // Ensure that the foreground color is suitably distinct from the background color
+ if (background != null) {
+ int bgRgb = ImageUtils.rgbToInt(background, 0xFF);
+ int backgroundBrightness = ImageUtils.getBrightness(bgRgb);
+ if (foreground == null) {
+ if (backgroundBrightness < 128) {
+ foreground = new RGB(255, 255, 255);
+ } else {
+ foreground = new RGB(0, 0, 0);
+ }
+ } else {
+ int fgRgb = ImageUtils.rgbToInt(foreground, 0xFF);
+ int foregroundBrightness = ImageUtils.getBrightness(fgRgb);
+ if (Math.abs(backgroundBrightness - foregroundBrightness) < 64) {
+ if (backgroundBrightness < 128) {
+ foreground = new RGB(255, 255, 255);
+ } else {
+ foreground = new RGB(0, 0, 0);
+ }
+ }
+ }
+ }
+
+ return Pair.of(background, foreground);
+ }
+
+ /**
+ * Renders the given resource which should refer to a drawable and returns a
+ * representative color value for the drawable (such as the color in the center)
+ *
+ * @param themeItemName the item in the theme to be looked up and rendered
+ * @return a color representing a typical color in the drawable
+ */
+ private RGB renderDrawableResource(String themeItemName) {
+ GraphicalEditorPart editor = mPalette.getEditor();
+ ResourceResolver resources = editor.getResourceResolver();
+ ResourceValue resourceValue = resources.findItemInTheme(themeItemName);
+ BufferedImage image = RenderService.create(editor)
+ .setOverrideRenderSize(100, 100)
+ .renderDrawable(resourceValue);
+ if (image != null) {
+ // Use the middle pixel as the color since that works better for gradients;
+ // solid colors work too.
+ int rgb = image.getRGB(image.getWidth() / 2, image.getHeight() / 2);
+ return ImageUtils.intToRgb(rgb);
+ }
+
+ return null;
+ }
+
+ private static RGB resolveThemeColor(ResourceResolver resources, String resourceName) {
+ ResourceValue textColor = resources.findItemInTheme(resourceName);
+ return ResourceHelper.resolveColor(resources, textColor);
+ }
+
+ private String getFileName(ElementDescriptor descriptor) {
+ if (descriptor instanceof PaletteMetadataDescriptor) {
+ PaletteMetadataDescriptor pmd = (PaletteMetadataDescriptor) descriptor;
+ StringBuilder sb = new StringBuilder();
+ String name = pmd.getUiName();
+ // Strip out whitespace, parentheses, etc.
+ for (int i = 0, n = name.length(); i < n; i++) {
+ char c = name.charAt(i);
+ if (Character.isLetter(c)) {
+ sb.append(c);
+ }
+ }
+ return sb.toString() + DOT_PNG;
+ }
+ return descriptor.getUiName() + DOT_PNG;
+ }
+
+ private String getFileName(UiElementNode node) {
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ String fqn = repository.getFullClassName((Element) node.getXmlNode());
+ return fqn.substring(fqn.lastIndexOf('.') + 1) + DOT_PNG;
+ }
+
+ /**
+ * Cleans up a name by removing punctuation and whitespace etc to make
+ * it a better filename
+ * @param name the name to clean
+ * @return a cleaned up name
+ */
+ @NonNull
+ private static String cleanup(@Nullable String name) {
+ if (name == null) {
+ return "";
+ }
+
+ // Extract just the characters (no whitespace, parentheses, punctuation etc)
+ // to ensure that the filename is pretty portable
+ StringBuilder sb = new StringBuilder(name.length());
+ for (int i = 0; i < name.length(); i++) {
+ char c = name.charAt(i);
+ if (Character.isJavaIdentifierPart(c)) {
+ sb.append(Character.toLowerCase(c));
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /** Returns the location of a directory containing image previews (which may not exist) */
+ private File getImageDir(boolean create) {
+ if (mImageDir == null) {
+ // Location for plugin-related state data
+ IPath pluginState = AdtPlugin.getDefault().getStateLocation();
+
+ // We have multiple directories - one for each combination of SDK, theme and device
+ // (and later, possibly other qualifiers).
+ // These are created -lazily-.
+ String targetName = mPalette.getCurrentTarget().hashString();
+ String androidTargetNamePrefix = "android-";
+ String themeNamePrefix = "Theme.";
+ if (targetName.startsWith(androidTargetNamePrefix)) {
+ targetName = targetName.substring(androidTargetNamePrefix.length());
+ }
+ String themeName = mPalette.getCurrentTheme();
+ if (themeName == null) {
+ themeName = "Theme"; //$NON-NLS-1$
+ }
+ if (themeName.startsWith(themeNamePrefix)) {
+ themeName = themeName.substring(themeNamePrefix.length());
+ }
+ targetName = cleanup(targetName);
+ themeName = cleanup(themeName);
+ String deviceName = cleanup(mPalette.getCurrentDevice());
+ String dirName = String.format("palette-preview-r16b-%s-%s-%s", targetName,
+ themeName, deviceName);
+ IPath dirPath = pluginState.append(dirName);
+
+ mImageDir = new File(dirPath.toOSString());
+ }
+
+ if (create && !mImageDir.exists()) {
+ mImageDir.mkdirs();
+ }
+
+ return mImageDir;
+ }
+
+ private void savePreview(File output, BufferedImage image,
+ int left, int top, int right, int bottom) {
+ try {
+ BufferedImage im = ImageUtils.subImage(image, left, top, right, bottom);
+ ImageIO.write(im, "PNG", output); //$NON-NLS-1$
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Failed writing palette file");
+ }
+ }
+
+ private void storeBackground(File imageDir, RGB bg, RGB fg) {
+ mBackground = bg;
+ mForeground = fg;
+ File file = new File(imageDir, PREVIEW_INFO_FILE);
+ String colors = String.format(
+ "background=#%02x%02x%02x\nforeground=#%02x%02x%02x\n", //$NON-NLS-1$
+ bg.red, bg.green, bg.blue,
+ fg.red, fg.green, fg.blue);
+ AdtPlugin.writeFile(file, colors);
+ }
+
+ public RGB getBackgroundColor() {
+ if (mBackground == null) {
+ initColors();
+ }
+
+ return mBackground;
+ }
+
+ public RGB getForegroundColor() {
+ if (mForeground == null) {
+ initColors();
+ }
+
+ return mForeground;
+ }
+
+ public void initColors() {
+ try {
+ // Already initialized? Foreground can be null which would call
+ // initColors again and again, but background is never null after
+ // initialization so we use it as the have-initialized flag.
+ if (mBackground != null) {
+ return;
+ }
+
+ File imageDir = getImageDir(false);
+ if (!imageDir.exists()) {
+ render();
+
+ // Initialized as part of the render
+ if (mBackground != null) {
+ return;
+ }
+ }
+
+ File file = new File(imageDir, PREVIEW_INFO_FILE);
+ if (file.exists()) {
+ Properties properties = new Properties();
+ InputStream is = null;
+ try {
+ is = new BufferedInputStream(new FileInputStream(file));
+ properties.load(is);
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Can't read preview properties");
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ // Nothing useful can be done.
+ }
+ }
+ }
+
+ String colorString = (String) properties.get("background"); //$NON-NLS-1$
+ if (colorString != null) {
+ int rgb = ImageUtils.getColor(colorString.trim());
+ mBackground = ImageUtils.intToRgb(rgb);
+ }
+ colorString = (String) properties.get("foreground"); //$NON-NLS-1$
+ if (colorString != null) {
+ int rgb = ImageUtils.getColor(colorString.trim());
+ mForeground = ImageUtils.intToRgb(rgb);
+ }
+ }
+
+ if (mBackground == null) {
+ mBackground = new RGB(0, 0, 0);
+ }
+ // mForeground is allowed to be null.
+ } catch (Throwable t) {
+ AdtPlugin.log(t, "Cannot initialize preview color settings");
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLogger.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLogger.java
new file mode 100644
index 000000000..8548830bd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderLogger.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2010 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.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.RenderSecurityManager;
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.core.runtime.IStatus;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A {@link LayoutLog} which records the problems it encounters and offers them as a
+ * single summary at the end
+ */
+public class RenderLogger extends LayoutLog {
+ static final String TAG_MISSING_DIMENSION = "missing.dimension"; //$NON-NLS-1$
+
+ private final String mName;
+ private List<String> mFidelityWarnings;
+ private List<String> mWarnings;
+ private List<String> mErrors;
+ private boolean mHaveExceptions;
+ private List<String> mTags;
+ private List<Throwable> mTraces;
+ private static Set<String> sIgnoredFidelityWarnings;
+ private final Object mCredential;
+
+ /** Construct a logger for the given named layout */
+ RenderLogger(String name, Object credential) {
+ mName = name;
+ mCredential = credential;
+ }
+
+ /**
+ * Are there any logged errors or warnings during the render?
+ *
+ * @return true if there were problems during the render
+ */
+ public boolean hasProblems() {
+ return mFidelityWarnings != null || mErrors != null || mWarnings != null ||
+ mHaveExceptions;
+ }
+
+ /**
+ * Returns a list of traces encountered during rendering, or null if none
+ *
+ * @return a list of traces encountered during rendering, or null if none
+ */
+ @Nullable
+ public List<Throwable> getFirstTrace() {
+ return mTraces;
+ }
+
+ /**
+ * Returns a (possibly multi-line) description of all the problems
+ *
+ * @param includeFidelityWarnings if true, include fidelity warnings in the problem
+ * summary
+ * @return a string describing the rendering problems
+ */
+ @NonNull
+ public String getProblems(boolean includeFidelityWarnings) {
+ StringBuilder sb = new StringBuilder();
+
+ if (mErrors != null) {
+ for (String error : mErrors) {
+ sb.append(error).append('\n');
+ }
+ }
+
+ if (mWarnings != null) {
+ for (String warning : mWarnings) {
+ sb.append(warning).append('\n');
+ }
+ }
+
+ if (includeFidelityWarnings && mFidelityWarnings != null) {
+ sb.append("The graphics preview in the layout editor may not be accurate:\n");
+ for (String warning : mFidelityWarnings) {
+ sb.append("* ");
+ sb.append(warning).append('\n');
+ }
+ }
+
+ if (mHaveExceptions) {
+ sb.append("Exception details are logged in Window > Show View > Error Log");
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Returns the fidelity warnings
+ *
+ * @return the fidelity warnings
+ */
+ @Nullable
+ public List<String> getFidelityWarnings() {
+ return mFidelityWarnings;
+ }
+
+ // ---- extends LayoutLog ----
+
+ @Override
+ public void error(String tag, String message, Object data) {
+ String description = describe(message);
+
+ appendToIdeLog(null, IStatus.ERROR, description);
+
+ // Workaround: older layout libraries don't provide a tag for this error
+ if (tag == null && message != null
+ && message.startsWith("Failed to find style ")) { //$NON-NLS-1$
+ tag = LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR;
+ }
+
+ addError(tag, description);
+ }
+
+ @Override
+ public void error(String tag, String message, Throwable throwable, Object data) {
+ String description = describe(message);
+ appendToIdeLog(throwable, IStatus.ERROR, description);
+
+ if (throwable != null) {
+ if (throwable instanceof ClassNotFoundException) {
+ // The project callback is given a chance to resolve classes,
+ // and when it fails, it will record it in its own list which
+ // is displayed in a special way (with action hyperlinks etc).
+ // Therefore, include these messages in the visible render log,
+ // especially since the user message from a ClassNotFoundException
+ // is really not helpful (it just lists the class name without
+ // even mentioning that it is a class-not-found exception.)
+ return;
+ }
+
+ if (description.equals(throwable.getLocalizedMessage()) ||
+ description.equals(throwable.getMessage())) {
+ description = "Exception raised during rendering: " + description;
+ }
+ recordThrowable(throwable);
+ mHaveExceptions = true;
+ }
+
+ addError(tag, description);
+ }
+
+ /**
+ * Record that the given exception was encountered during rendering
+ *
+ * @param throwable the exception that was raised
+ */
+ public void recordThrowable(@NonNull Throwable throwable) {
+ if (mTraces == null) {
+ mTraces = new ArrayList<Throwable>();
+ }
+ mTraces.add(throwable);
+ }
+
+ @Override
+ public void warning(String tag, String message, Object data) {
+ String description = describe(message);
+
+ boolean log = true;
+ if (TAG_RESOURCES_FORMAT.equals(tag)) {
+ if (description.equals("You must supply a layout_width attribute.") //$NON-NLS-1$
+ || description.equals("You must supply a layout_height attribute.")) {//$NON-NLS-1$
+ tag = TAG_MISSING_DIMENSION;
+ log = false;
+ }
+ }
+
+ if (log) {
+ appendToIdeLog(null, IStatus.WARNING, description);
+ }
+
+ addWarning(tag, description);
+ }
+
+ @Override
+ public void fidelityWarning(String tag, String message, Throwable throwable, Object data) {
+ if (sIgnoredFidelityWarnings != null && sIgnoredFidelityWarnings.contains(message)) {
+ return;
+ }
+
+ String description = describe(message);
+ appendToIdeLog(throwable, IStatus.ERROR, description);
+
+ if (throwable != null) {
+ mHaveExceptions = true;
+ }
+
+ addFidelityWarning(tag, description);
+ }
+
+ /**
+ * Ignore the given render fidelity warning for the current session
+ *
+ * @param message the message to be ignored for this session
+ */
+ public static void ignoreFidelityWarning(String message) {
+ if (sIgnoredFidelityWarnings == null) {
+ sIgnoredFidelityWarnings = new HashSet<String>();
+ }
+ sIgnoredFidelityWarnings.add(message);
+ }
+
+ @NonNull
+ private String describe(@Nullable String message) {
+ if (message == null) {
+ return "";
+ } else {
+ return message;
+ }
+ }
+
+ private void addWarning(String tag, String description) {
+ if (mWarnings == null) {
+ mWarnings = new ArrayList<String>();
+ } else if (mWarnings.contains(description)) {
+ // Avoid duplicates
+ return;
+ }
+ mWarnings.add(description);
+ addTag(tag);
+ }
+
+ private void addError(String tag, String description) {
+ if (mErrors == null) {
+ mErrors = new ArrayList<String>();
+ } else if (mErrors.contains(description)) {
+ // Avoid duplicates
+ return;
+ }
+ mErrors.add(description);
+ addTag(tag);
+ }
+
+ private void addFidelityWarning(String tag, String description) {
+ if (mFidelityWarnings == null) {
+ mFidelityWarnings = new ArrayList<String>();
+ } else if (mFidelityWarnings.contains(description)) {
+ // Avoid duplicates
+ return;
+ }
+ mFidelityWarnings.add(description);
+ addTag(tag);
+ }
+
+ // ---- Tags ----
+
+ private void addTag(String tag) {
+ if (tag != null) {
+ if (mTags == null) {
+ mTags = new ArrayList<String>();
+ }
+ mTags.add(tag);
+ }
+ }
+
+ /**
+ * Returns true if the given tag prefix has been seen
+ *
+ * @param prefix the tag prefix to look for
+ * @return true iff any tags with the given prefix was seen during the render
+ */
+ public boolean seenTagPrefix(String prefix) {
+ if (mTags != null) {
+ for (String tag : mTags) {
+ if (tag.startsWith(prefix)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the given tag has been seen
+ *
+ * @param tag the tag to look for
+ * @return true iff the tag was seen during the render
+ */
+ public boolean seenTag(String tag) {
+ if (mTags != null) {
+ return mTags.contains(tag);
+ } else {
+ return false;
+ }
+ }
+
+ // Append the given message to the ADT log. Bypass the sandbox if necessary
+ // such that we can write to the log file.
+ private void appendToIdeLog(Throwable throwable, int severity, String description) {
+ boolean token = RenderSecurityManager.enterSafeRegion(mCredential);
+ try {
+ if (throwable != null) {
+ AdtPlugin.log(throwable, "%1$s: %2$s", mName, description);
+ } else {
+ AdtPlugin.log(severity, "%1$s: %2$s", mName, description);
+ }
+ } finally {
+ RenderSecurityManager.exitSafeRegion(token);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java
new file mode 100644
index 000000000..5621d5f17
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java
@@ -0,0 +1,1333 @@
+/*
+ * Copyright (C) 2012 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 static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_RENDERING;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SMALL_SHADOW_SIZE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.DEFAULT;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.INCLUDES;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.rendering.api.Result;
+import com.android.ide.common.rendering.api.Result.Status;
+import com.android.ide.common.resources.ResourceFile;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.ResourceResolver;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.ScreenOrientationQualifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Locale;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.NestedConfiguration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.VaryingConfiguration;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.io.IFileWrapper;
+import com.android.io.IAbstractFile;
+import com.android.resources.Density;
+import com.android.resources.ResourceType;
+import com.android.resources.ScreenOrientation;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.Screen;
+import com.android.sdklib.devices.State;
+import com.android.utils.SdkUtils;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.jobs.IJobChangeEvent;
+import org.eclipse.core.runtime.jobs.IJobChangeListener;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.dialogs.InputDialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Region;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.progress.UIJob;
+import org.w3c.dom.Document;
+
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.lang.ref.SoftReference;
+import java.util.Comparator;
+import java.util.Map;
+
+/**
+ * Represents a preview rendering of a given configuration
+ */
+public class RenderPreview implements IJobChangeListener {
+ /** Whether previews should use large shadows */
+ static final boolean LARGE_SHADOWS = false;
+
+ /**
+ * Still doesn't work; get exceptions from layoutlib:
+ * java.lang.IllegalStateException: After scene creation, #init() must be called
+ * at com.android.layoutlib.bridge.impl.RenderAction.acquire(RenderAction.java:151)
+ * <p>
+ * TODO: Investigate.
+ */
+ private static final boolean RENDER_ASYNC = false;
+
+ /**
+ * Height of the toolbar shown over a preview during hover. Needs to be
+ * large enough to accommodate icons below.
+ */
+ private static final int HEADER_HEIGHT = 20;
+
+ /** Whether to dump out rendering failures of the previews to the log */
+ private static final boolean DUMP_RENDER_DIAGNOSTICS = false;
+
+ /** Extra error checking in debug mode */
+ private static final boolean DEBUG = false;
+
+ private static final Image EDIT_ICON;
+ private static final Image ZOOM_IN_ICON;
+ private static final Image ZOOM_OUT_ICON;
+ private static final Image CLOSE_ICON;
+ private static final int EDIT_ICON_WIDTH;
+ private static final int ZOOM_IN_ICON_WIDTH;
+ private static final int ZOOM_OUT_ICON_WIDTH;
+ private static final int CLOSE_ICON_WIDTH;
+ static {
+ ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
+ IconFactory icons = IconFactory.getInstance();
+ CLOSE_ICON = sharedImages.getImage(ISharedImages.IMG_ETOOL_DELETE);
+ EDIT_ICON = icons.getIcon("editPreview"); //$NON-NLS-1$
+ ZOOM_IN_ICON = icons.getIcon("zoomplus"); //$NON-NLS-1$
+ ZOOM_OUT_ICON = icons.getIcon("zoomminus"); //$NON-NLS-1$
+ CLOSE_ICON_WIDTH = CLOSE_ICON.getImageData().width;
+ EDIT_ICON_WIDTH = EDIT_ICON.getImageData().width;
+ ZOOM_IN_ICON_WIDTH = ZOOM_IN_ICON.getImageData().width;
+ ZOOM_OUT_ICON_WIDTH = ZOOM_OUT_ICON.getImageData().width;
+ }
+
+ /** The configuration being previewed */
+ private @NonNull Configuration mConfiguration;
+
+ /** Configuration to use if we have an alternate input to be rendered */
+ private @NonNull Configuration mAlternateConfiguration;
+
+ /** The associated manager */
+ private final @NonNull RenderPreviewManager mManager;
+ private final @NonNull LayoutCanvas mCanvas;
+
+ private @NonNull SoftReference<ResourceResolver> mResourceResolver =
+ new SoftReference<ResourceResolver>(null);
+ private @Nullable Job mJob;
+ private @Nullable Image mThumbnail;
+ private @Nullable String mDisplayName;
+ private int mWidth;
+ private int mHeight;
+ private int mX;
+ private int mY;
+ private int mTitleHeight;
+ private double mScale = 1.0;
+ private double mAspectRatio;
+
+ /** If non null, points to a separate file containing the source */
+ private @Nullable IFile mAlternateInput;
+
+ /** If included within another layout, the name of that outer layout */
+ private @Nullable Reference mIncludedWithin;
+
+ /** Whether the mouse is actively hovering over this preview */
+ private boolean mActive;
+
+ /**
+ * Whether this preview cannot be rendered because of a model error - such
+ * as an invalid configuration, a missing resource, an error in the XML
+ * markup, etc. If non null, contains the error message (or a blank string
+ * if not known), and null if the render was successful.
+ */
+ private String mError;
+
+ /** Whether in the current layout, this preview is visible */
+ private boolean mVisible;
+
+ /** Whether the configuration has changed and needs to be refreshed the next time
+ * this preview made visible. This corresponds to the change flags in
+ * {@link ConfigurationClient}. */
+ private int mDirty;
+
+ /**
+ * Creates a new {@linkplain RenderPreview}
+ *
+ * @param manager the manager
+ * @param canvas canvas where preview is painted
+ * @param configuration the associated configuration
+ * @param width the initial width to use for the preview
+ * @param height the initial height to use for the preview
+ */
+ private RenderPreview(
+ @NonNull RenderPreviewManager manager,
+ @NonNull LayoutCanvas canvas,
+ @NonNull Configuration configuration) {
+ mManager = manager;
+ mCanvas = canvas;
+ mConfiguration = configuration;
+ updateSize();
+
+ // Should only attempt to create configurations for fully configured devices
+ assert mConfiguration.getDevice() != null
+ && mConfiguration.getDeviceState() != null
+ && mConfiguration.getLocale() != null
+ && mConfiguration.getTarget() != null
+ && mConfiguration.getTheme() != null
+ && mConfiguration.getFullConfig() != null
+ && mConfiguration.getFullConfig().getScreenSizeQualifier() != null :
+ mConfiguration;
+ }
+
+ /**
+ * Sets the configuration to use for this preview
+ *
+ * @param configuration the new configuration
+ */
+ public void setConfiguration(@NonNull Configuration configuration) {
+ mConfiguration = configuration;
+ }
+
+ /**
+ * Gets the scale being applied to the thumbnail
+ *
+ * @return the scale being applied to the thumbnail
+ */
+ public double getScale() {
+ return mScale;
+ }
+
+ /**
+ * Sets the scale to apply to the thumbnail
+ *
+ * @param scale the factor to scale the thumbnail picture by
+ */
+ public void setScale(double scale) {
+ disposeThumbnail();
+ mScale = scale;
+ }
+
+ /**
+ * Returns the aspect ratio of this render preview
+ *
+ * @return the aspect ratio
+ */
+ public double getAspectRatio() {
+ return mAspectRatio;
+ }
+
+ /**
+ * Returns whether the preview is actively hovered
+ *
+ * @return whether the mouse is hovering over the preview
+ */
+ public boolean isActive() {
+ return mActive;
+ }
+
+ /**
+ * Sets whether the preview is actively hovered
+ *
+ * @param active if the mouse is hovering over the preview
+ */
+ public void setActive(boolean active) {
+ mActive = active;
+ }
+
+ /**
+ * Returns whether the preview is visible. Previews that are off
+ * screen are typically marked invisible during layout, which means we don't
+ * have to expend effort computing preview thumbnails etc
+ *
+ * @return true if the preview is visible
+ */
+ public boolean isVisible() {
+ return mVisible;
+ }
+
+ /**
+ * Returns whether this preview represents a forked layout
+ *
+ * @return true if this preview represents a separate file
+ */
+ public boolean isForked() {
+ return mAlternateInput != null || mIncludedWithin != null;
+ }
+
+ /**
+ * Returns the file to be used for this preview, or null if this is not a
+ * forked layout meaning that the file is the one used in the chooser
+ *
+ * @return the file or null for non-forked layouts
+ */
+ @Nullable
+ public IFile getAlternateInput() {
+ if (mAlternateInput != null) {
+ return mAlternateInput;
+ } else if (mIncludedWithin != null) {
+ return mIncludedWithin.getFile();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the area of this render preview, PRIOR to scaling
+ *
+ * @return the area (width times height without scaling)
+ */
+ int getArea() {
+ return mWidth * mHeight;
+ }
+
+ /**
+ * Sets whether the preview is visible. Previews that are off
+ * screen are typically marked invisible during layout, which means we don't
+ * have to expend effort computing preview thumbnails etc
+ *
+ * @param visible whether this preview is visible
+ */
+ public void setVisible(boolean visible) {
+ if (visible != mVisible) {
+ mVisible = visible;
+ if (mVisible) {
+ if (mDirty != 0) {
+ // Just made the render preview visible:
+ configurationChanged(mDirty); // schedules render
+ } else {
+ updateForkStatus();
+ mManager.scheduleRender(this);
+ }
+ } else {
+ dispose();
+ }
+ }
+ }
+
+ /**
+ * Sets the layout position relative to the top left corner of the preview
+ * area, in control coordinates
+ */
+ void setPosition(int x, int y) {
+ mX = x;
+ mY = y;
+ }
+
+ /**
+ * Gets the layout X position relative to the top left corner of the preview
+ * area, in control coordinates
+ */
+ int getX() {
+ return mX;
+ }
+
+ /**
+ * Gets the layout Y position relative to the top left corner of the preview
+ * area, in control coordinates
+ */
+ int getY() {
+ return mY;
+ }
+
+ /** Determine whether this configuration has a better match in a different layout file */
+ private void updateForkStatus() {
+ ConfigurationChooser chooser = mManager.getChooser();
+ FolderConfiguration config = mConfiguration.getFullConfig();
+ if (mAlternateInput != null && chooser.isBestMatchFor(mAlternateInput, config)) {
+ return;
+ }
+
+ mAlternateInput = null;
+ IFile editedFile = chooser.getEditedFile();
+ if (editedFile != null) {
+ if (!chooser.isBestMatchFor(editedFile, config)) {
+ ProjectResources resources = chooser.getResources();
+ if (resources != null) {
+ ResourceFile best = resources.getMatchingFile(editedFile.getName(),
+ ResourceType.LAYOUT, config);
+ if (best != null) {
+ IAbstractFile file = best.getFile();
+ if (file instanceof IFileWrapper) {
+ mAlternateInput = ((IFileWrapper) file).getIFile();
+ } else if (file instanceof File) {
+ mAlternateInput = AdtUtils.fileToIFile(((File) file));
+ }
+ }
+ }
+ if (mAlternateInput != null) {
+ mAlternateConfiguration = Configuration.create(mConfiguration,
+ mAlternateInput);
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a new {@linkplain RenderPreview}
+ *
+ * @param manager the manager
+ * @param configuration the associated configuration
+ * @return a new configuration
+ */
+ @NonNull
+ public static RenderPreview create(
+ @NonNull RenderPreviewManager manager,
+ @NonNull Configuration configuration) {
+ LayoutCanvas canvas = manager.getCanvas();
+ return new RenderPreview(manager, canvas, configuration);
+ }
+
+ /**
+ * Throws away this preview: cancels any pending rendering jobs and disposes
+ * of image resources etc
+ */
+ public void dispose() {
+ disposeThumbnail();
+
+ if (mJob != null) {
+ mJob.cancel();
+ mJob = null;
+ }
+ }
+
+ /** Disposes the thumbnail rendering. */
+ void disposeThumbnail() {
+ if (mThumbnail != null) {
+ mThumbnail.dispose();
+ mThumbnail = null;
+ }
+ }
+
+ /**
+ * Returns the display name of this preview
+ *
+ * @return the name of the preview
+ */
+ @NonNull
+ public String getDisplayName() {
+ if (mDisplayName == null) {
+ String displayName = getConfiguration().getDisplayName();
+ if (displayName == null) {
+ // No display name: this must be the configuration used by default
+ // for the view which is originally displayed (before adding thumbnails),
+ // and you've switched away to something else; now we need to display a name
+ // for this original configuration. For now, just call it "Original"
+ return "Original";
+ }
+
+ return displayName;
+ }
+
+ return mDisplayName;
+ }
+
+ /**
+ * Sets the display name of this preview. By default, the display name is
+ * the display name of the configuration, but it can be overridden by calling
+ * this setter (which only sets the preview name, without editing the configuration.)
+ *
+ * @param displayName the new display name
+ */
+ public void setDisplayName(@NonNull String displayName) {
+ mDisplayName = displayName;
+ }
+
+ /**
+ * Sets an inclusion context to use for this layout, if any. This will render
+ * the configuration preview as the outer layout with the current layout
+ * embedded within.
+ *
+ * @param includedWithin a reference to a layout which includes this one
+ */
+ public void setIncludedWithin(Reference includedWithin) {
+ mIncludedWithin = includedWithin;
+ }
+
+ /**
+ * Request a new render after the given delay
+ *
+ * @param delay the delay to wait before starting the render job
+ */
+ public void render(long delay) {
+ Job job = mJob;
+ if (job != null) {
+ job.cancel();
+ }
+ if (RENDER_ASYNC) {
+ job = new AsyncRenderJob();
+ } else {
+ job = new RenderJob();
+ }
+ job.schedule(delay);
+ job.addJobChangeListener(this);
+ mJob = job;
+ }
+
+ /** Render immediately */
+ private void renderSync() {
+ GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ if (editor.getReadyLayoutLib(false /*displayError*/) == null) {
+ // Don't attempt to render when there is no ready layout library: most likely
+ // the targets are loading/reloading.
+ return;
+ }
+
+ disposeThumbnail();
+
+ Configuration configuration =
+ mAlternateInput != null && mAlternateConfiguration != null
+ ? mAlternateConfiguration : mConfiguration;
+ ResourceResolver resolver = getResourceResolver(configuration);
+ RenderService renderService = RenderService.create(editor, configuration, resolver);
+
+ if (mIncludedWithin != null) {
+ renderService.setIncludedWithin(mIncludedWithin);
+ }
+
+ if (mAlternateInput != null) {
+ IAndroidTarget target = editor.getRenderingTarget();
+ AndroidTargetData data = null;
+ if (target != null) {
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ data = sdk.getTargetData(target);
+ }
+ }
+
+ // Construct UI model from XML
+ DocumentDescriptor documentDescriptor;
+ if (data == null) {
+ documentDescriptor = new DocumentDescriptor("temp", null);//$NON-NLS-1$
+ } else {
+ documentDescriptor = data.getLayoutDescriptors().getDescriptor();
+ }
+ UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode();
+ model.setEditor(mCanvas.getEditorDelegate().getEditor());
+ model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider());
+
+ Document document = DomUtilities.getDocument(mAlternateInput);
+ if (document == null) {
+ mError = "No document";
+ createErrorThumbnail();
+ return;
+ }
+ model.loadFromXmlNode(document);
+ renderService.setModel(model);
+ } else {
+ renderService.setModel(editor.getModel());
+ }
+ RenderLogger log = editor.createRenderLogger(getDisplayName());
+ renderService.setLog(log);
+ RenderSession session = renderService.createRenderSession();
+ Result render = session.render(1000);
+
+ if (DUMP_RENDER_DIAGNOSTICS) {
+ if (log.hasProblems() || !render.isSuccess()) {
+ AdtPlugin.log(IStatus.ERROR, "Found problems rendering preview "
+ + getDisplayName() + ": "
+ + render.getErrorMessage() + " : "
+ + log.getProblems(false));
+ Throwable exception = render.getException();
+ if (exception != null) {
+ AdtPlugin.log(exception, "Failure rendering preview " + getDisplayName());
+ }
+ }
+ }
+
+ if (render.isSuccess()) {
+ mError = null;
+ } else {
+ mError = render.getErrorMessage();
+ if (mError == null) {
+ mError = "";
+ }
+ }
+
+ if (render.getStatus() == Status.ERROR_TIMEOUT) {
+ // TODO: Special handling? schedule update again later
+ return;
+ }
+ if (render.isSuccess()) {
+ BufferedImage image = session.getImage();
+ if (image != null) {
+ createThumbnail(image);
+ }
+ }
+
+ if (mError != null) {
+ createErrorThumbnail();
+ }
+ }
+
+ private ResourceResolver getResourceResolver(Configuration configuration) {
+ ResourceResolver resourceResolver = mResourceResolver.get();
+ if (resourceResolver != null) {
+ return resourceResolver;
+ }
+
+ GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ String theme = configuration.getTheme();
+ if (theme == null) {
+ return null;
+ }
+
+ Map<ResourceType, Map<String, ResourceValue>> configuredFrameworkRes = null;
+ Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes = null;
+
+ FolderConfiguration config = configuration.getFullConfig();
+ IAndroidTarget target = graphicalEditor.getRenderingTarget();
+ ResourceRepository frameworkRes = null;
+ if (target != null) {
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk == null) {
+ return null;
+ }
+ AndroidTargetData data = sdk.getTargetData(target);
+
+ if (data != null) {
+ // TODO: SHARE if possible
+ frameworkRes = data.getFrameworkResources();
+ configuredFrameworkRes = frameworkRes.getConfiguredResources(config);
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ assert configuredFrameworkRes != null;
+
+
+ // get the resources of the file's project.
+ ProjectResources projectRes = ResourceManager.getInstance().getProjectResources(
+ graphicalEditor.getProject());
+ configuredProjectRes = projectRes.getConfiguredResources(config);
+
+ if (!theme.startsWith(PREFIX_RESOURCE_REF)) {
+ if (frameworkRes.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + theme)) {
+ theme = ANDROID_STYLE_RESOURCE_PREFIX + theme;
+ } else {
+ theme = STYLE_RESOURCE_PREFIX + theme;
+ }
+ }
+
+ resourceResolver = ResourceResolver.create(
+ configuredProjectRes, configuredFrameworkRes,
+ ResourceHelper.styleToTheme(theme),
+ ResourceHelper.isProjectStyle(theme));
+ mResourceResolver = new SoftReference<ResourceResolver>(resourceResolver);
+ return resourceResolver;
+ }
+
+ /**
+ * Sets the new image of the preview and generates a thumbnail
+ *
+ * @param image the full size image
+ */
+ void createThumbnail(BufferedImage image) {
+ if (image == null) {
+ mThumbnail = null;
+ return;
+ }
+
+ ImageOverlay imageOverlay = mCanvas.getImageOverlay();
+ boolean drawShadows = imageOverlay == null || imageOverlay.getShowDropShadow();
+ double scale = getWidth() / (double) image.getWidth();
+ int shadowSize;
+ if (LARGE_SHADOWS) {
+ shadowSize = drawShadows ? SHADOW_SIZE : 0;
+ } else {
+ shadowSize = drawShadows ? SMALL_SHADOW_SIZE : 0;
+ }
+ if (scale < 1.0) {
+ if (LARGE_SHADOWS) {
+ image = ImageUtils.scale(image, scale, scale,
+ shadowSize, shadowSize);
+ if (drawShadows) {
+ ImageUtils.drawRectangleShadow(image, 0, 0,
+ image.getWidth() - shadowSize,
+ image.getHeight() - shadowSize);
+ }
+ } else {
+ image = ImageUtils.scale(image, scale, scale,
+ shadowSize, shadowSize);
+ if (drawShadows) {
+ ImageUtils.drawSmallRectangleShadow(image, 0, 0,
+ image.getWidth() - shadowSize,
+ image.getHeight() - shadowSize);
+ }
+ }
+ }
+
+ mThumbnail = SwtUtils.convertToSwt(mCanvas.getDisplay(), image,
+ true /* transferAlpha */, -1);
+ }
+
+ void createErrorThumbnail() {
+ int shadowSize = LARGE_SHADOWS ? SHADOW_SIZE : SMALL_SHADOW_SIZE;
+ int width = getWidth();
+ int height = getHeight();
+ BufferedImage image = new BufferedImage(width + shadowSize, height + shadowSize,
+ BufferedImage.TYPE_INT_ARGB);
+
+ Graphics2D g = image.createGraphics();
+ g.setColor(new java.awt.Color(0xfffbfcc6));
+ g.fillRect(0, 0, width, height);
+
+ g.dispose();
+
+ ImageOverlay imageOverlay = mCanvas.getImageOverlay();
+ boolean drawShadows = imageOverlay == null || imageOverlay.getShowDropShadow();
+ if (drawShadows) {
+ if (LARGE_SHADOWS) {
+ ImageUtils.drawRectangleShadow(image, 0, 0,
+ image.getWidth() - SHADOW_SIZE,
+ image.getHeight() - SHADOW_SIZE);
+ } else {
+ ImageUtils.drawSmallRectangleShadow(image, 0, 0,
+ image.getWidth() - SMALL_SHADOW_SIZE,
+ image.getHeight() - SMALL_SHADOW_SIZE);
+ }
+ }
+
+ mThumbnail = SwtUtils.convertToSwt(mCanvas.getDisplay(), image,
+ true /* transferAlpha */, -1);
+ }
+
+ private static double getScale(int width, int height) {
+ int maxWidth = RenderPreviewManager.getMaxWidth();
+ int maxHeight = RenderPreviewManager.getMaxHeight();
+ if (width > 0 && height > 0
+ && (width > maxWidth || height > maxHeight)) {
+ if (width >= height) { // landscape
+ return maxWidth / (double) width;
+ } else { // portrait
+ return maxHeight / (double) height;
+ }
+ }
+
+ return 1.0;
+ }
+
+ /**
+ * Returns the width of the preview, in pixels
+ *
+ * @return the width in pixels
+ */
+ public int getWidth() {
+ return (int) (mWidth * mScale * RenderPreviewManager.getScale());
+ }
+
+ /**
+ * Returns the height of the preview, in pixels
+ *
+ * @return the height in pixels
+ */
+ public int getHeight() {
+ return (int) (mHeight * mScale * RenderPreviewManager.getScale());
+ }
+
+ /**
+ * Handles clicks within the preview (x and y are positions relative within the
+ * preview
+ *
+ * @param x the x coordinate within the preview where the click occurred
+ * @param y the y coordinate within the preview where the click occurred
+ * @return true if this preview handled (and therefore consumed) the click
+ */
+ public boolean click(int x, int y) {
+ if (y >= mTitleHeight && y < mTitleHeight + HEADER_HEIGHT) {
+ int left = 0;
+ left += CLOSE_ICON_WIDTH;
+ if (x <= left) {
+ // Delete
+ mManager.deletePreview(this);
+ return true;
+ }
+ left += ZOOM_IN_ICON_WIDTH;
+ if (x <= left) {
+ // Zoom in
+ mScale = mScale * (1 / 0.5);
+ if (Math.abs(mScale-1.0) < 0.0001) {
+ mScale = 1.0;
+ }
+
+ render(0);
+ mManager.layout(true);
+ mCanvas.redraw();
+ return true;
+ }
+ left += ZOOM_OUT_ICON_WIDTH;
+ if (x <= left) {
+ // Zoom out
+ mScale = mScale * (0.5 / 1);
+ if (Math.abs(mScale-1.0) < 0.0001) {
+ mScale = 1.0;
+ }
+ render(0);
+
+ mManager.layout(true);
+ mCanvas.redraw();
+ return true;
+ }
+ left += EDIT_ICON_WIDTH;
+ if (x <= left) {
+ // Edit. For now, just rename
+ InputDialog d = new InputDialog(
+ AdtPlugin.getShell(),
+ "Rename Preview", // title
+ "Name:",
+ getDisplayName(),
+ null);
+ if (d.open() == Window.OK) {
+ String newName = d.getValue();
+ mConfiguration.setDisplayName(newName);
+ if (mDescription != null) {
+ mManager.rename(mDescription, newName);
+ }
+ mCanvas.redraw();
+ }
+
+ return true;
+ }
+
+ // Clicked anywhere else on header
+ // Perhaps open Edit dialog here?
+ }
+
+ mManager.switchTo(this);
+ return true;
+ }
+
+ /**
+ * Paints the preview at the given x/y position
+ *
+ * @param gc the graphics context to paint it into
+ * @param x the x coordinate to paint the preview at
+ * @param y the y coordinate to paint the preview at
+ */
+ void paint(GC gc, int x, int y) {
+ mTitleHeight = paintTitle(gc, x, y, true /*showFile*/);
+ y += mTitleHeight;
+ y += 2;
+
+ int width = getWidth();
+ int height = getHeight();
+ if (mThumbnail != null && mError == null) {
+ gc.drawImage(mThumbnail, x, y);
+
+ if (mActive) {
+ int oldWidth = gc.getLineWidth();
+ gc.setLineWidth(3);
+ gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_LIST_SELECTION));
+ gc.drawRectangle(x - 1, y - 1, width + 2, height + 2);
+ gc.setLineWidth(oldWidth);
+ }
+ } else if (mError != null) {
+ if (mThumbnail != null) {
+ gc.drawImage(mThumbnail, x, y);
+ } else {
+ gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER));
+ gc.drawRectangle(x, y, width, height);
+ }
+
+ gc.setClipping(x, y, width, height);
+ Image icon = IconFactory.getInstance().getIcon("renderError"); //$NON-NLS-1$
+ ImageData data = icon.getImageData();
+ int prevAlpha = gc.getAlpha();
+ int alpha = 96;
+ if (mThumbnail != null) {
+ alpha -= 32;
+ }
+ gc.setAlpha(alpha);
+ gc.drawImage(icon, x + (width - data.width) / 2, y + (height - data.height) / 2);
+
+ String msg = mError;
+ Density density = mConfiguration.getDensity();
+ if (density == Density.TV || density == Density.LOW) {
+ msg = "Broken rendering library; unsupported DPI. Try using the SDK manager " +
+ "to get updated layout libraries.";
+ }
+ int charWidth = gc.getFontMetrics().getAverageCharWidth();
+ int charsPerLine = (width - 10) / charWidth;
+ msg = SdkUtils.wrap(msg, charsPerLine, null);
+ gc.setAlpha(255);
+ gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_BLACK));
+ gc.drawText(msg, x + 5, y + HEADER_HEIGHT, true);
+ gc.setAlpha(prevAlpha);
+ gc.setClipping((Region) null);
+ } else {
+ gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER));
+ gc.drawRectangle(x, y, width, height);
+
+ Image icon = IconFactory.getInstance().getIcon("refreshPreview"); //$NON-NLS-1$
+ ImageData data = icon.getImageData();
+ int prevAlpha = gc.getAlpha();
+ gc.setAlpha(96);
+ gc.drawImage(icon, x + (width - data.width) / 2,
+ y + (height - data.height) / 2);
+ gc.setAlpha(prevAlpha);
+ }
+
+ if (mActive) {
+ int left = x ;
+ int prevAlpha = gc.getAlpha();
+ gc.setAlpha(208);
+ Color bg = mCanvas.getDisplay().getSystemColor(SWT.COLOR_WHITE);
+ gc.setBackground(bg);
+ gc.fillRectangle(left, y, x + width - left, HEADER_HEIGHT);
+ gc.setAlpha(prevAlpha);
+
+ y += 2;
+
+ // Paint icons
+ gc.drawImage(CLOSE_ICON, left, y);
+ left += CLOSE_ICON_WIDTH;
+
+ gc.drawImage(ZOOM_IN_ICON, left, y);
+ left += ZOOM_IN_ICON_WIDTH;
+
+ gc.drawImage(ZOOM_OUT_ICON, left, y);
+ left += ZOOM_OUT_ICON_WIDTH;
+
+ gc.drawImage(EDIT_ICON, left, y);
+ left += EDIT_ICON_WIDTH;
+ }
+ }
+
+ /**
+ * Paints the preview title at the given position (and returns the required
+ * height)
+ *
+ * @param gc the graphics context to paint into
+ * @param x the left edge of the preview rectangle
+ * @param y the top edge of the preview rectangle
+ */
+ private int paintTitle(GC gc, int x, int y, boolean showFile) {
+ String displayName = getDisplayName();
+ return paintTitle(gc, x, y, showFile, displayName);
+ }
+
+ /**
+ * Paints the preview title at the given position (and returns the required
+ * height)
+ *
+ * @param gc the graphics context to paint into
+ * @param x the left edge of the preview rectangle
+ * @param y the top edge of the preview rectangle
+ * @param displayName the title string to be used
+ */
+ int paintTitle(GC gc, int x, int y, boolean showFile, String displayName) {
+ int titleHeight = 0;
+
+ if (showFile && mIncludedWithin != null) {
+ if (mManager.getMode() != INCLUDES) {
+ displayName = "<include>";
+ } else {
+ // Skip: just paint footer instead
+ displayName = null;
+ }
+ }
+
+ int width = getWidth();
+ int labelTop = y + 1;
+ gc.setClipping(x, labelTop, width, 100);
+
+ // Use font height rather than extent height since we want two adjacent
+ // previews (which may have different display names and therefore end
+ // up with slightly different extent heights) to have identical title
+ // heights such that they are aligned identically
+ int fontHeight = gc.getFontMetrics().getHeight();
+
+ if (displayName != null && displayName.length() > 0) {
+ gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_WHITE));
+ Point extent = gc.textExtent(displayName);
+ int labelLeft = Math.max(x, x + (width - extent.x) / 2);
+ Image icon = null;
+ Locale locale = mConfiguration.getLocale();
+ if (locale != null && (locale.hasLanguage() || locale.hasRegion())
+ && (!(mConfiguration instanceof NestedConfiguration)
+ || ((NestedConfiguration) mConfiguration).isOverridingLocale())) {
+ icon = locale.getFlagImage();
+ }
+
+ if (icon != null) {
+ int flagWidth = icon.getImageData().width;
+ int flagHeight = icon.getImageData().height;
+ labelLeft = Math.max(x + flagWidth / 2, labelLeft);
+ gc.drawImage(icon, labelLeft - flagWidth / 2 - 1, labelTop);
+ labelLeft += flagWidth / 2 + 1;
+ gc.drawText(displayName, labelLeft,
+ labelTop - (extent.y - flagHeight) / 2, true);
+ } else {
+ gc.drawText(displayName, labelLeft, labelTop, true);
+ }
+
+ labelTop += extent.y;
+ titleHeight += fontHeight;
+ }
+
+ if (showFile && (mAlternateInput != null || mIncludedWithin != null)) {
+ // Draw file flag, and parent folder name
+ IFile file = mAlternateInput != null
+ ? mAlternateInput : mIncludedWithin.getFile();
+ String fileName = file.getParent().getName() + File.separator
+ + file.getName();
+ Point extent = gc.textExtent(fileName);
+ Image icon = IconFactory.getInstance().getIcon("android_file"); //$NON-NLS-1$
+ int flagWidth = icon.getImageData().width;
+ int flagHeight = icon.getImageData().height;
+
+ int labelLeft = Math.max(x, x + (width - extent.x - flagWidth - 1) / 2);
+
+ gc.drawImage(icon, labelLeft, labelTop);
+
+ gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY));
+ labelLeft += flagWidth + 1;
+ labelTop -= (extent.y - flagHeight) / 2;
+ gc.drawText(fileName, labelLeft, labelTop, true);
+
+ titleHeight += Math.max(titleHeight, icon.getImageData().height);
+ }
+
+ gc.setClipping((Region) null);
+
+ return titleHeight;
+ }
+
+ /**
+ * Notifies that the preview's configuration has changed.
+ *
+ * @param flags the change flags, a bitmask corresponding to the
+ * {@code CHANGE_} constants in {@link ConfigurationClient}
+ */
+ public void configurationChanged(int flags) {
+ if (!mVisible) {
+ mDirty |= flags;
+ return;
+ }
+
+ if ((flags & MASK_RENDERING) != 0) {
+ mResourceResolver.clear();
+ // Handle inheritance
+ mConfiguration.syncFolderConfig();
+ updateForkStatus();
+ updateSize();
+ }
+
+ // Sanity check to make sure things are working correctly
+ if (DEBUG) {
+ RenderPreviewMode mode = mManager.getMode();
+ if (mode == DEFAULT) {
+ assert mConfiguration instanceof VaryingConfiguration;
+ VaryingConfiguration config = (VaryingConfiguration) mConfiguration;
+ int alternateFlags = config.getAlternateFlags();
+ switch (alternateFlags) {
+ case Configuration.CFG_DEVICE_STATE: {
+ State configState = config.getDeviceState();
+ State chooserState = mManager.getChooser().getConfiguration()
+ .getDeviceState();
+ assert configState != null && chooserState != null;
+ assert !configState.getName().equals(chooserState.getName())
+ : configState.toString() + ':' + chooserState;
+
+ Device configDevice = config.getDevice();
+ Device chooserDevice = mManager.getChooser().getConfiguration()
+ .getDevice();
+ assert configDevice != null && chooserDevice != null;
+ assert configDevice == chooserDevice
+ : configDevice.toString() + ':' + chooserDevice;
+
+ break;
+ }
+ case Configuration.CFG_DEVICE: {
+ Device configDevice = config.getDevice();
+ Device chooserDevice = mManager.getChooser().getConfiguration()
+ .getDevice();
+ assert configDevice != null && chooserDevice != null;
+ assert configDevice != chooserDevice
+ : configDevice.toString() + ':' + chooserDevice;
+
+ State configState = config.getDeviceState();
+ State chooserState = mManager.getChooser().getConfiguration()
+ .getDeviceState();
+ assert configState != null && chooserState != null;
+ assert configState.getName().equals(chooserState.getName())
+ : configState.toString() + ':' + chooserState;
+
+ break;
+ }
+ case Configuration.CFG_LOCALE: {
+ Locale configLocale = config.getLocale();
+ Locale chooserLocale = mManager.getChooser().getConfiguration()
+ .getLocale();
+ assert configLocale != null && chooserLocale != null;
+ assert configLocale != chooserLocale
+ : configLocale.toString() + ':' + chooserLocale;
+ break;
+ }
+ default: {
+ // Some other type of override I didn't anticipate
+ assert false : alternateFlags;
+ }
+ }
+ }
+ }
+
+ mDirty = 0;
+ mManager.scheduleRender(this);
+ }
+
+ private void updateSize() {
+ Device device = mConfiguration.getDevice();
+ if (device == null) {
+ return;
+ }
+ Screen screen = device.getDefaultHardware().getScreen();
+ if (screen == null) {
+ return;
+ }
+
+ FolderConfiguration folderConfig = mConfiguration.getFullConfig();
+ ScreenOrientationQualifier qualifier = folderConfig.getScreenOrientationQualifier();
+ ScreenOrientation orientation = qualifier == null
+ ? ScreenOrientation.PORTRAIT : qualifier.getValue();
+
+ // compute width and height to take orientation into account.
+ int x = screen.getXDimension();
+ int y = screen.getYDimension();
+ int screenWidth, screenHeight;
+
+ if (x > y) {
+ if (orientation == ScreenOrientation.LANDSCAPE) {
+ screenWidth = x;
+ screenHeight = y;
+ } else {
+ screenWidth = y;
+ screenHeight = x;
+ }
+ } else {
+ if (orientation == ScreenOrientation.LANDSCAPE) {
+ screenWidth = y;
+ screenHeight = x;
+ } else {
+ screenWidth = x;
+ screenHeight = y;
+ }
+ }
+
+ int width = RenderPreviewManager.getMaxWidth();
+ int height = RenderPreviewManager.getMaxHeight();
+ if (screenWidth > 0) {
+ double scale = getScale(screenWidth, screenHeight);
+ width = (int) (screenWidth * scale);
+ height = (int) (screenHeight * scale);
+ }
+
+ if (width != mWidth || height != mHeight) {
+ mWidth = width;
+ mHeight = height;
+
+ Image thumbnail = mThumbnail;
+ mThumbnail = null;
+ if (thumbnail != null) {
+ thumbnail.dispose();
+ }
+ if (mHeight != 0) {
+ mAspectRatio = mWidth / (double) mHeight;
+ }
+ }
+ }
+
+ /**
+ * Returns the configuration associated with this preview
+ *
+ * @return the configuration
+ */
+ @NonNull
+ public Configuration getConfiguration() {
+ return mConfiguration;
+ }
+
+ // ---- Implements IJobChangeListener ----
+
+ @Override
+ public void aboutToRun(IJobChangeEvent event) {
+ }
+
+ @Override
+ public void awake(IJobChangeEvent event) {
+ }
+
+ @Override
+ public void done(IJobChangeEvent event) {
+ mJob = null;
+ }
+
+ @Override
+ public void running(IJobChangeEvent event) {
+ }
+
+ @Override
+ public void scheduled(IJobChangeEvent event) {
+ }
+
+ @Override
+ public void sleeping(IJobChangeEvent event) {
+ }
+
+ // ---- Delayed Rendering ----
+
+ private final class RenderJob extends UIJob {
+ public RenderJob() {
+ super("RenderPreview");
+ setSystem(true);
+ setUser(false);
+ }
+
+ @Override
+ public IStatus runInUIThread(IProgressMonitor monitor) {
+ mJob = null;
+ if (!mCanvas.isDisposed()) {
+ renderSync();
+ mCanvas.redraw();
+ return org.eclipse.core.runtime.Status.OK_STATUS;
+ }
+
+ return org.eclipse.core.runtime.Status.CANCEL_STATUS;
+ }
+
+ @Override
+ public Display getDisplay() {
+ if (mCanvas.isDisposed()) {
+ return null;
+ }
+ return mCanvas.getDisplay();
+ }
+ }
+
+ private final class AsyncRenderJob extends Job {
+ public AsyncRenderJob() {
+ super("RenderPreview");
+ setSystem(true);
+ setUser(false);
+ }
+
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ mJob = null;
+
+ if (mCanvas.isDisposed()) {
+ return org.eclipse.core.runtime.Status.CANCEL_STATUS;
+ }
+
+ renderSync();
+
+ // Update display
+ mCanvas.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ mCanvas.redraw();
+ }
+ });
+
+ return org.eclipse.core.runtime.Status.OK_STATUS;
+ }
+ }
+
+ /**
+ * Sets the input file to use for rendering. If not set, this will just be
+ * the same file as the configuration chooser. This is used to render other
+ * layouts, such as variations of the currently edited layout, which are
+ * not kept in sync with the main layout.
+ *
+ * @param file the file to set as input
+ */
+ public void setAlternateInput(@Nullable IFile file) {
+ mAlternateInput = file;
+ }
+
+ /** Corresponding description for this preview if it is a manually added preview */
+ private @Nullable ConfigurationDescription mDescription;
+
+ /**
+ * Sets the description of this preview, if this preview is a manually added preview
+ *
+ * @param description the description of this preview
+ */
+ public void setDescription(@Nullable ConfigurationDescription description) {
+ mDescription = description;
+ }
+
+ /**
+ * Returns the description of this preview, if this preview is a manually added preview
+ *
+ * @return the description
+ */
+ @Nullable
+ public ConfigurationDescription getDescription() {
+ return mDescription;
+ }
+
+ @Override
+ public String toString() {
+ return getDisplayName() + ':' + mConfiguration;
+ }
+
+ /** Sorts render previews into increasing aspect ratio order */
+ static Comparator<RenderPreview> INCREASING_ASPECT_RATIO = new Comparator<RenderPreview>() {
+ @Override
+ public int compare(RenderPreview preview1, RenderPreview preview2) {
+ return (int) Math.signum(preview1.mAspectRatio - preview2.mAspectRatio);
+ }
+ };
+ /** Sorts render previews into visual order: row by row, column by column */
+ static Comparator<RenderPreview> VISUAL_ORDER = new Comparator<RenderPreview>() {
+ @Override
+ public int compare(RenderPreview preview1, RenderPreview preview2) {
+ int delta = preview1.mY - preview2.mY;
+ if (delta == 0) {
+ delta = preview1.mX - preview2.mX;
+ }
+ return delta;
+ }
+ };
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewList.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewList.java
new file mode 100644
index 000000000..2bcdba382
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewList.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2012 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.annotations.NonNull;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
+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.sdklib.devices.Device;
+import com.google.common.base.Charsets;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.QualifiedName;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/** A list of render previews */
+class RenderPreviewList {
+ /** Name of file saved in project directory storing previews */
+ private static final String PREVIEW_FILE_NAME = "previews.xml"; //$NON-NLS-1$
+
+ /** Qualified name for the per-project persistent property include-map */
+ private final static QualifiedName PREVIEW_LIST = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ "previewlist");//$NON-NLS-1$
+
+ private final IProject mProject;
+ private final List<ConfigurationDescription> mList = Lists.newArrayList();
+
+ private RenderPreviewList(@NonNull IProject project) {
+ mProject = project;
+ }
+
+ /**
+ * Returns the {@link RenderPreviewList} for the given project
+ *
+ * @param project the project the list is associated with
+ * @return a {@link RenderPreviewList} for the given project, never null
+ */
+ @NonNull
+ public static RenderPreviewList get(@NonNull IProject project) {
+ RenderPreviewList list = null;
+ try {
+ list = (RenderPreviewList) project.getSessionProperty(PREVIEW_LIST);
+ } catch (CoreException e) {
+ // Not a problem; we will just create a new one
+ }
+
+ if (list == null) {
+ list = new RenderPreviewList(project);
+ try {
+ project.setSessionProperty(PREVIEW_LIST, list);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ return list;
+ }
+
+ private File getManualFile() {
+ return new File(AdtUtils.getAbsolutePath(mProject).toFile(), PREVIEW_FILE_NAME);
+ }
+
+ void load(Collection<Device> deviceList) throws IOException {
+ File file = getManualFile();
+ if (file.exists()) {
+ load(file, deviceList);
+ }
+ }
+
+ void save() throws IOException {
+ deleteFile();
+ if (!mList.isEmpty()) {
+ File file = getManualFile();
+ save(file);
+ }
+ }
+
+ private void save(File file) throws IOException {
+ //Document document = DomUtilities.createEmptyPlainDocument();
+ Document document = DomUtilities.createEmptyDocument();
+ if (document != null) {
+ for (ConfigurationDescription description : mList) {
+ description.toXml(document);
+ }
+ String xml = EclipseXmlPrettyPrinter.prettyPrint(document, true);
+ Files.write(xml, file, Charsets.UTF_8);
+ }
+ }
+
+ void load(File file, Collection<Device> deviceList) throws IOException {
+ mList.clear();
+
+ String xml = Files.toString(file, Charsets.UTF_8);
+ Document document = DomUtilities.parseDocument(xml, true);
+ if (document == null || document.getDocumentElement() == null) {
+ return;
+ }
+ List<Element> elements = DomUtilities.getChildren(document.getDocumentElement());
+ for (Element element : elements) {
+ ConfigurationDescription description = ConfigurationDescription.fromXml(
+ mProject, element, deviceList);
+ if (description != null) {
+ mList.add(description);
+ }
+ }
+ }
+
+ /**
+ * Create a list of previews for the given canvas that matches the internal
+ * configuration preview list
+ *
+ * @param canvas the associated canvas
+ * @return a new list of previews linked to the given canvas
+ */
+ @NonNull
+ List<RenderPreview> createPreviews(LayoutCanvas canvas) {
+ if (mList.isEmpty()) {
+ return new ArrayList<RenderPreview>();
+ }
+ List<RenderPreview> previews = Lists.newArrayList();
+ RenderPreviewManager manager = canvas.getPreviewManager();
+ ConfigurationChooser chooser = canvas.getEditorDelegate().getGraphicalEditor()
+ .getConfigurationChooser();
+
+ Configuration chooserConfig = chooser.getConfiguration();
+ for (ConfigurationDescription description : mList) {
+ Configuration configuration = Configuration.create(chooser);
+ configuration.setDisplayName(description.displayName);
+ configuration.setActivity(description.activity);
+ configuration.setLocale(
+ description.locale != null ? description.locale : chooserConfig.getLocale(),
+ true);
+ // TODO: Make sure this layout isn't in some v-folder which is incompatible
+ // with this target!
+ configuration.setTarget(
+ description.target != null ? description.target : chooserConfig.getTarget(),
+ true);
+ configuration.setTheme(
+ description.theme != null ? description.theme : chooserConfig.getTheme());
+ configuration.setDevice(
+ description.device != null ? description.device : chooserConfig.getDevice(),
+ true);
+ configuration.setDeviceState(
+ description.state != null ? description.state : chooserConfig.getDeviceState(),
+ true);
+ configuration.setNightMode(
+ description.nightMode != null ? description.nightMode
+ : chooserConfig.getNightMode(), true);
+ configuration.setUiMode(
+ description.uiMode != null ? description.uiMode : chooserConfig.getUiMode(), true);
+
+ //configuration.syncFolderConfig();
+ configuration.getFullConfig().set(description.folder);
+
+ RenderPreview preview = RenderPreview.create(manager, configuration);
+
+ preview.setDescription(description);
+ previews.add(preview);
+ }
+
+ return previews;
+ }
+
+ void remove(@NonNull RenderPreview preview) {
+ ConfigurationDescription description = preview.getDescription();
+ if (description != null) {
+ mList.remove(description);
+ }
+ }
+
+ boolean isEmpty() {
+ return mList.isEmpty();
+ }
+
+ void add(@NonNull RenderPreview preview) {
+ Configuration configuration = preview.getConfiguration();
+ ConfigurationDescription description =
+ ConfigurationDescription.fromConfiguration(mProject, configuration);
+ // RenderPreviews can have display names that aren't reflected in the configuration
+ description.displayName = preview.getDisplayName();
+ mList.add(description);
+ preview.setDescription(description);
+ }
+
+ void delete() {
+ mList.clear();
+ deleteFile();
+ }
+
+ private void deleteFile() {
+ File file = getManualFile();
+ if (file.exists()) {
+ file.delete();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java
new file mode 100644
index 000000000..98dde86e0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java
@@ -0,0 +1,1696 @@
+/*
+ * Copyright (C) 2012 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 static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE_STATE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_ALL;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SMALL_SHADOW_SIZE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreview.LARGE_SHADOWS;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.CUSTOM;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.NONE;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.SCREENS;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.resources.configuration.DensityQualifier;
+import com.android.ide.common.resources.configuration.DeviceConfigHelper;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+import com.android.ide.common.resources.configuration.ScreenSizeQualifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Locale;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.NestedConfiguration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.VaryingConfiguration;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.resources.Density;
+import com.android.resources.ScreenSize;
+import com.android.sdklib.devices.Device;
+import com.android.sdklib.devices.Screen;
+import com.android.sdklib.devices.State;
+import com.google.common.collect.Lists;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.dialogs.InputDialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.ui.IWorkbenchPartSite;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.ide.IDE;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Manager for the configuration previews, which handles layout computations,
+ * managing the image buffer cache, etc
+ */
+public class RenderPreviewManager {
+ private static double sScale = 1.0;
+ private static final int RENDER_DELAY = 150;
+ private static final int PREVIEW_VGAP = 18;
+ private static final int PREVIEW_HGAP = 12;
+ private static final int MAX_WIDTH = 200;
+ private static final int MAX_HEIGHT = MAX_WIDTH;
+ private static final int ZOOM_ICON_WIDTH = 16;
+ private static final int ZOOM_ICON_HEIGHT = 16;
+ private @Nullable List<RenderPreview> mPreviews;
+ private @Nullable RenderPreviewList mManualList;
+ private final @NonNull LayoutCanvas mCanvas;
+ private final @NonNull CanvasTransform mVScale;
+ private final @NonNull CanvasTransform mHScale;
+ private int mPrevCanvasWidth;
+ private int mPrevCanvasHeight;
+ private int mPrevImageWidth;
+ private int mPrevImageHeight;
+ private @NonNull RenderPreviewMode mMode = NONE;
+ private @Nullable RenderPreview mActivePreview;
+ private @Nullable ScrollBarListener mListener;
+ private int mLayoutHeight;
+ /** Last seen state revision in this {@link RenderPreviewManager}. If less
+ * than {@link #sRevision}, the previews need to be updated on next exposure */
+ private static int mRevision;
+ /** Current global revision count */
+ private static int sRevision;
+ private boolean mNeedLayout;
+ private boolean mNeedRender;
+ private boolean mNeedZoom;
+ private SwapAnimation mAnimation;
+
+ /**
+ * Creates a {@link RenderPreviewManager} associated with the given canvas
+ *
+ * @param canvas the canvas to manage previews for
+ */
+ public RenderPreviewManager(@NonNull LayoutCanvas canvas) {
+ mCanvas = canvas;
+ mHScale = canvas.getHorizontalTransform();
+ mVScale = canvas.getVerticalTransform();
+ }
+
+ /**
+ * Revise the global state revision counter. This will cause all layout
+ * preview managers to refresh themselves to the latest revision when they
+ * are next exposed.
+ */
+ public static void bumpRevision() {
+ sRevision++;
+ }
+
+ /**
+ * Returns the associated chooser
+ *
+ * @return the associated chooser
+ */
+ @NonNull
+ ConfigurationChooser getChooser() {
+ GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ return editor.getConfigurationChooser();
+ }
+
+ /**
+ * Returns the associated canvas
+ *
+ * @return the canvas
+ */
+ @NonNull
+ public LayoutCanvas getCanvas() {
+ return mCanvas;
+ }
+
+ /** Zooms in (grows all previews) */
+ public void zoomIn() {
+ sScale = sScale * (1 / 0.9);
+ if (Math.abs(sScale-1.0) < 0.0001) {
+ sScale = 1.0;
+ }
+
+ updatedZoom();
+ }
+
+ /** Zooms out (shrinks all previews) */
+ public void zoomOut() {
+ sScale = sScale * (0.9 / 1);
+ if (Math.abs(sScale-1.0) < 0.0001) {
+ sScale = 1.0;
+ }
+ updatedZoom();
+ }
+
+ /** Zooms to 100 (resets zoom) */
+ public void zoomReset() {
+ sScale = 1.0;
+ updatedZoom();
+ mNeedZoom = mNeedLayout = true;
+ mCanvas.redraw();
+ }
+
+ private void updatedZoom() {
+ if (hasPreviews()) {
+ for (RenderPreview preview : mPreviews) {
+ preview.disposeThumbnail();
+ }
+ RenderPreview preview = mCanvas.getPreview();
+ if (preview != null) {
+ preview.disposeThumbnail();
+ }
+ }
+
+ mNeedLayout = mNeedRender = true;
+ mCanvas.redraw();
+ }
+
+ static int getMaxWidth() {
+ return (int) (sScale * MAX_WIDTH);
+ }
+
+ static int getMaxHeight() {
+ return (int) (sScale * MAX_HEIGHT);
+ }
+
+ static double getScale() {
+ return sScale;
+ }
+
+ /**
+ * Returns whether there are any manual preview items (provided the current
+ * mode is manual previews
+ *
+ * @return true if there are items in the manual preview list
+ */
+ public boolean hasManualPreviews() {
+ assert mMode == CUSTOM;
+ return mManualList != null && !mManualList.isEmpty();
+ }
+
+ /** Delete all the previews */
+ public void deleteManualPreviews() {
+ disposePreviews();
+ selectMode(NONE);
+ mCanvas.setFitScale(true /* onlyZoomOut */, true /*allowZoomIn*/);
+
+ if (mManualList != null) {
+ mManualList.delete();
+ }
+ }
+
+ /** Dispose all the previews */
+ public void disposePreviews() {
+ if (mPreviews != null) {
+ List<RenderPreview> old = mPreviews;
+ mPreviews = null;
+ for (RenderPreview preview : old) {
+ preview.dispose();
+ }
+ }
+ }
+
+ /**
+ * Deletes the given preview
+ *
+ * @param preview the preview to be deleted
+ */
+ public void deletePreview(RenderPreview preview) {
+ mPreviews.remove(preview);
+ preview.dispose();
+ layout(true);
+ mCanvas.redraw();
+
+ if (mManualList != null) {
+ mManualList.remove(preview);
+ saveList();
+ }
+ }
+
+ /**
+ * Compute the total width required for the previews, including internal padding
+ *
+ * @return total width in pixels
+ */
+ public int computePreviewWidth() {
+ int maxPreviewWidth = 0;
+ if (hasPreviews()) {
+ for (RenderPreview preview : mPreviews) {
+ maxPreviewWidth = Math.max(maxPreviewWidth, preview.getWidth());
+ }
+
+ if (maxPreviewWidth > 0) {
+ maxPreviewWidth += 2 * PREVIEW_HGAP; // 2x for left and right side
+ maxPreviewWidth += LARGE_SHADOWS ? SHADOW_SIZE : SMALL_SHADOW_SIZE;
+ }
+
+ return maxPreviewWidth;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Layout Algorithm. This sets the {@link RenderPreview#getX()} and
+ * {@link RenderPreview#getY()} coordinates of all the previews. It also
+ * marks previews as visible or invisible via
+ * {@link RenderPreview#setVisible(boolean)} according to their position and
+ * the current visible view port in the layout canvas. Finally, it also sets
+ * the {@code mLayoutHeight} field, such that the scrollbars can compute the
+ * right scrolled area, and that scrolling can cause render refreshes on
+ * views that are made visible.
+ * <p>
+ * This is not a traditional bin packing problem, because the objects to be
+ * packaged do not have a fixed size; we can scale them up and down in order
+ * to provide an "optimal" size.
+ * <p>
+ * See http://en.wikipedia.org/wiki/Packing_problem See
+ * http://en.wikipedia.org/wiki/Bin_packing_problem
+ */
+ void layout(boolean refresh) {
+ mNeedLayout = false;
+
+ if (mPreviews == null || mPreviews.isEmpty()) {
+ return;
+ }
+
+ int scaledImageWidth = mHScale.getScaledImgSize();
+ int scaledImageHeight = mVScale.getScaledImgSize();
+ Rectangle clientArea = mCanvas.getClientArea();
+
+ if (!refresh &&
+ (scaledImageWidth == mPrevImageWidth
+ && scaledImageHeight == mPrevImageHeight
+ && clientArea.width == mPrevCanvasWidth
+ && clientArea.height == mPrevCanvasHeight)) {
+ // No change
+ return;
+ }
+
+ mPrevImageWidth = scaledImageWidth;
+ mPrevImageHeight = scaledImageHeight;
+ mPrevCanvasWidth = clientArea.width;
+ mPrevCanvasHeight = clientArea.height;
+
+ if (mListener == null) {
+ mListener = new ScrollBarListener();
+ mCanvas.getVerticalBar().addSelectionListener(mListener);
+ }
+
+ beginRenderScheduling();
+
+ mLayoutHeight = 0;
+
+ if (previewsHaveIdenticalSize() || fixedOrder()) {
+ // If all the preview boxes are of identical sizes, or if the order is predetermined,
+ // just lay them out in rows.
+ rowLayout();
+ } else if (previewsFit()) {
+ layoutFullFit();
+ } else {
+ rowLayout();
+ }
+
+ mCanvas.updateScrollBars();
+ }
+
+ /**
+ * Performs a simple layout where the views are laid out in a row, wrapping
+ * around the top left canvas image.
+ */
+ private void rowLayout() {
+ // TODO: Separate layout heuristics for portrait and landscape orientations (though
+ // it also depends on the dimensions of the canvas window, which determines the
+ // shape of the leftover space)
+
+ int scaledImageWidth = mHScale.getScaledImgSize();
+ int scaledImageHeight = mVScale.getScaledImgSize();
+ Rectangle clientArea = mCanvas.getClientArea();
+
+ int availableWidth = clientArea.x + clientArea.width - getX();
+ int availableHeight = clientArea.y + clientArea.height - getY();
+ int maxVisibleY = clientArea.y + clientArea.height;
+
+ int bottomBorder = scaledImageHeight;
+ int rightHandSide = scaledImageWidth + PREVIEW_HGAP;
+ int nextY = 0;
+
+ // First lay out images across the top right hand side
+ int x = rightHandSide;
+ int y = 0;
+ boolean wrapped = false;
+
+ int vgap = PREVIEW_VGAP;
+ for (RenderPreview preview : mPreviews) {
+ // If we have forked previews, double the vgap to allow space for two labels
+ if (preview.isForked()) {
+ vgap *= 2;
+ break;
+ }
+ }
+
+ List<RenderPreview> aspectOrder;
+ if (!fixedOrder()) {
+ aspectOrder = new ArrayList<RenderPreview>(mPreviews);
+ Collections.sort(aspectOrder, RenderPreview.INCREASING_ASPECT_RATIO);
+ } else {
+ aspectOrder = mPreviews;
+ }
+
+ for (RenderPreview preview : aspectOrder) {
+ if (x > 0 && x + preview.getWidth() > availableWidth) {
+ x = rightHandSide;
+ int prevY = y;
+ y = nextY;
+ if ((prevY <= bottomBorder ||
+ y <= bottomBorder)
+ && Math.max(nextY, y + preview.getHeight()) > bottomBorder) {
+ // If there's really no visible room below, don't bother
+ // Similarly, don't wrap individually scaled views
+ if (bottomBorder < availableHeight - 40 && preview.getScale() < 1.2) {
+ // If it's closer to the top row than the bottom, just
+ // mark the next row for left justify instead
+ if (bottomBorder - y > y + preview.getHeight() - bottomBorder) {
+ rightHandSide = 0;
+ wrapped = true;
+ } else if (!wrapped) {
+ y = nextY = Math.max(nextY, bottomBorder + vgap);
+ x = rightHandSide = 0;
+ wrapped = true;
+ }
+ }
+ }
+ }
+ if (x > 0 && y <= bottomBorder
+ && Math.max(nextY, y + preview.getHeight()) > bottomBorder) {
+ if (clientArea.height - bottomBorder < preview.getHeight()) {
+ // No room below the device on the left; just continue on the
+ // bottom row
+ } else if (preview.getScale() < 1.2) {
+ if (bottomBorder - y > y + preview.getHeight() - bottomBorder) {
+ rightHandSide = 0;
+ wrapped = true;
+ } else {
+ y = nextY = Math.max(nextY, bottomBorder + vgap);
+ x = rightHandSide = 0;
+ wrapped = true;
+ }
+ }
+ }
+
+ preview.setPosition(x, y);
+
+ if (y > maxVisibleY && maxVisibleY > 0) {
+ preview.setVisible(false);
+ } else if (!preview.isVisible()) {
+ preview.setVisible(true);
+ }
+
+ x += preview.getWidth();
+ x += PREVIEW_HGAP;
+ nextY = Math.max(nextY, y + preview.getHeight() + vgap);
+ }
+
+ mLayoutHeight = nextY;
+ }
+
+ private boolean fixedOrder() {
+ return mMode == SCREENS;
+ }
+
+ /** Returns true if all the previews have the same identical size */
+ private boolean previewsHaveIdenticalSize() {
+ if (!hasPreviews()) {
+ return true;
+ }
+
+ Iterator<RenderPreview> iterator = mPreviews.iterator();
+ RenderPreview first = iterator.next();
+ int width = first.getWidth();
+ int height = first.getHeight();
+
+ while (iterator.hasNext()) {
+ RenderPreview preview = iterator.next();
+ if (width != preview.getWidth() || height != preview.getHeight()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /** Returns true if all the previews can fully fit in the available space */
+ private boolean previewsFit() {
+ int scaledImageWidth = mHScale.getScaledImgSize();
+ int scaledImageHeight = mVScale.getScaledImgSize();
+ Rectangle clientArea = mCanvas.getClientArea();
+ int availableWidth = clientArea.x + clientArea.width - getX();
+ int availableHeight = clientArea.y + clientArea.height - getY();
+ int bottomBorder = scaledImageHeight;
+ int rightHandSide = scaledImageWidth + PREVIEW_HGAP;
+
+ // First see if we can fit everything; if so, we can try to make the layouts
+ // larger such that they fill up all the available space
+ long availableArea = rightHandSide * bottomBorder +
+ availableWidth * (Math.max(0, availableHeight - bottomBorder));
+
+ long requiredArea = 0;
+ for (RenderPreview preview : mPreviews) {
+ // Note: This does not include individual preview scale; the layout
+ // algorithm itself may be tweaking the scales to fit elements within
+ // the layout
+ requiredArea += preview.getArea();
+ }
+
+ return requiredArea * sScale < availableArea;
+ }
+
+ private void layoutFullFit() {
+ int scaledImageWidth = mHScale.getScaledImgSize();
+ int scaledImageHeight = mVScale.getScaledImgSize();
+ Rectangle clientArea = mCanvas.getClientArea();
+ int availableWidth = clientArea.x + clientArea.width - getX();
+ int availableHeight = clientArea.y + clientArea.height - getY();
+ int maxVisibleY = clientArea.y + clientArea.height;
+ int bottomBorder = scaledImageHeight;
+ int rightHandSide = scaledImageWidth + PREVIEW_HGAP;
+
+ int minWidth = Integer.MAX_VALUE;
+ int minHeight = Integer.MAX_VALUE;
+ for (RenderPreview preview : mPreviews) {
+ minWidth = Math.min(minWidth, preview.getWidth());
+ minHeight = Math.min(minHeight, preview.getHeight());
+ }
+
+ BinPacker packer = new BinPacker(minWidth, minHeight);
+
+ // TODO: Instead of this, just start with client area and occupy scaled image size!
+
+ // Add in gap on right and bottom since we'll add that requirement on the width and
+ // height rectangles too (for spacing)
+ packer.addSpace(new Rect(rightHandSide, 0,
+ availableWidth - rightHandSide + PREVIEW_HGAP,
+ availableHeight + PREVIEW_VGAP));
+ if (maxVisibleY > bottomBorder) {
+ packer.addSpace(new Rect(0, bottomBorder + PREVIEW_VGAP,
+ availableWidth + PREVIEW_HGAP, maxVisibleY - bottomBorder + PREVIEW_VGAP));
+ }
+
+ // TODO: Sort previews first before attempting to position them?
+
+ ArrayList<RenderPreview> aspectOrder = new ArrayList<RenderPreview>(mPreviews);
+ Collections.sort(aspectOrder, RenderPreview.INCREASING_ASPECT_RATIO);
+
+ for (RenderPreview preview : aspectOrder) {
+ int previewWidth = preview.getWidth();
+ int previewHeight = preview.getHeight();
+ previewHeight += PREVIEW_VGAP;
+ if (preview.isForked()) {
+ previewHeight += PREVIEW_VGAP;
+ }
+ previewWidth += PREVIEW_HGAP;
+ // title height? how do I account for that?
+ Rect position = packer.occupy(previewWidth, previewHeight);
+ if (position != null) {
+ preview.setPosition(position.x, position.y);
+ preview.setVisible(true);
+ } else {
+ // Can't fit: give up and do plain row layout
+ rowLayout();
+ return;
+ }
+ }
+
+ mLayoutHeight = availableHeight;
+ }
+ /**
+ * Paints the configuration previews
+ *
+ * @param gc the graphics context to paint into
+ */
+ void paint(GC gc) {
+ if (hasPreviews()) {
+ // Ensure up to date at all times; consider moving if it's too expensive
+ layout(mNeedLayout);
+ if (mNeedRender) {
+ renderPreviews();
+ }
+ if (mNeedZoom) {
+ boolean allowZoomIn = true /*mMode == NONE*/;
+ mCanvas.setFitScale(false /*onlyZoomOut*/, allowZoomIn);
+ mNeedZoom = false;
+ }
+ int rootX = getX();
+ int rootY = getY();
+
+ for (RenderPreview preview : mPreviews) {
+ if (preview.isVisible()) {
+ int x = rootX + preview.getX();
+ int y = rootY + preview.getY();
+ preview.paint(gc, x, y);
+ }
+ }
+
+ RenderPreview preview = mCanvas.getPreview();
+ if (preview != null) {
+ String displayName = null;
+ Configuration configuration = preview.getConfiguration();
+ if (configuration instanceof VaryingConfiguration) {
+ // Use override flags from stashed preview, but configuration
+ // data from live (not varying) configured configuration
+ VaryingConfiguration cfg = (VaryingConfiguration) configuration;
+ int flags = cfg.getAlternateFlags() | cfg.getOverrideFlags();
+ displayName = NestedConfiguration.computeDisplayName(flags,
+ getChooser().getConfiguration());
+ } else if (configuration instanceof NestedConfiguration) {
+ int flags = ((NestedConfiguration) configuration).getOverrideFlags();
+ displayName = NestedConfiguration.computeDisplayName(flags,
+ getChooser().getConfiguration());
+ } else {
+ displayName = configuration.getDisplayName();
+ }
+ if (displayName != null) {
+ CanvasTransform hi = mHScale;
+ CanvasTransform vi = mVScale;
+
+ int destX = hi.translate(0);
+ int destY = vi.translate(0);
+ int destWidth = hi.getScaledImgSize();
+ int destHeight = vi.getScaledImgSize();
+
+ int x = destX + destWidth / 2 - preview.getWidth() / 2;
+ int y = destY + destHeight;
+
+ preview.paintTitle(gc, x, y, false /*showFile*/, displayName);
+ }
+ }
+
+ // Zoom overlay
+ int x = getZoomX();
+ if (x > 0) {
+ int y = getZoomY();
+ int oldAlpha = gc.getAlpha();
+
+ // Paint background oval rectangle behind the zoom and close icons
+ gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY));
+ gc.setAlpha(128);
+ int padding = 3;
+ int arc = 5;
+ gc.fillRoundRectangle(x - padding, y - padding,
+ ZOOM_ICON_WIDTH + 2 * padding,
+ 4 * ZOOM_ICON_HEIGHT + 2 * padding, arc, arc);
+
+ gc.setAlpha(255);
+ IconFactory iconFactory = IconFactory.getInstance();
+ Image zoomOut = iconFactory.getIcon("zoomminus"); //$NON-NLS-1$);
+ Image zoomIn = iconFactory.getIcon("zoomplus"); //$NON-NLS-1$);
+ Image zoom100 = iconFactory.getIcon("zoom100"); //$NON-NLS-1$);
+ Image close = iconFactory.getIcon("close"); //$NON-NLS-1$);
+
+ gc.drawImage(zoomIn, x, y);
+ y += ZOOM_ICON_HEIGHT;
+ gc.drawImage(zoomOut, x, y);
+ y += ZOOM_ICON_HEIGHT;
+ gc.drawImage(zoom100, x, y);
+ y += ZOOM_ICON_HEIGHT;
+ gc.drawImage(close, x, y);
+ y += ZOOM_ICON_HEIGHT;
+ gc.setAlpha(oldAlpha);
+ }
+ } else if (mMode == CUSTOM) {
+ int rootX = getX();
+ rootX += mHScale.getScaledImgSize();
+ rootX += 2 * PREVIEW_HGAP;
+ int rootY = getY();
+ rootY += 20;
+ gc.setFont(mCanvas.getFont());
+ gc.setForeground(mCanvas.getDisplay().getSystemColor(SWT.COLOR_BLACK));
+ gc.drawText("Add previews with \"Add as Thumbnail\"\nin the configuration menu",
+ rootX, rootY, true);
+ }
+
+ if (mAnimation != null) {
+ mAnimation.tick(gc);
+ }
+ }
+
+ private void addPreview(@NonNull RenderPreview preview) {
+ if (mPreviews == null) {
+ mPreviews = Lists.newArrayList();
+ }
+ mPreviews.add(preview);
+ }
+
+ /** Adds the current configuration as a new configuration preview */
+ public void addAsThumbnail() {
+ ConfigurationChooser chooser = getChooser();
+ String name = chooser.getConfiguration().getDisplayName();
+ if (name == null || name.isEmpty()) {
+ name = getUniqueName();
+ }
+ InputDialog d = new InputDialog(
+ AdtPlugin.getShell(),
+ "Add as Thumbnail Preview", // title
+ "Name of thumbnail:",
+ name,
+ null);
+ if (d.open() == Window.OK) {
+ selectMode(CUSTOM);
+
+ String newName = d.getValue();
+ // Create a new configuration from the current settings in the composite
+ Configuration configuration = Configuration.copy(chooser.getConfiguration());
+ configuration.setDisplayName(newName);
+
+ RenderPreview preview = RenderPreview.create(this, configuration);
+ addPreview(preview);
+
+ layout(true);
+ beginRenderScheduling();
+ scheduleRender(preview);
+ mCanvas.setFitScale(true /* onlyZoomOut */, false /*allowZoomIn*/);
+
+ if (mManualList == null) {
+ loadList();
+ }
+ if (mManualList != null) {
+ mManualList.add(preview);
+ saveList();
+ }
+ }
+ }
+
+ /**
+ * Computes a unique new name for a configuration preview that represents
+ * the current, default configuration
+ *
+ * @return a unique name
+ */
+ private String getUniqueName() {
+ if (mPreviews == null || mPreviews.isEmpty()) {
+ // NO, not for the first preview!
+ return "Config1";
+ }
+
+ Set<String> names = new HashSet<String>(mPreviews.size());
+ for (RenderPreview preview : mPreviews) {
+ names.add(preview.getDisplayName());
+ }
+
+ int index = 2;
+ while (true) {
+ String name = String.format("Config%1$d", index);
+ if (!names.contains(name)) {
+ return name;
+ }
+ index++;
+ }
+ }
+
+ /** Generates a bunch of default configuration preview thumbnails */
+ public void addDefaultPreviews() {
+ ConfigurationChooser chooser = getChooser();
+ Configuration parent = chooser.getConfiguration();
+ if (parent instanceof NestedConfiguration) {
+ parent = ((NestedConfiguration) parent).getParent();
+ }
+ if (mCanvas.getImageOverlay().getImage() != null) {
+ // Create Language variation
+ createLocaleVariation(chooser, parent);
+
+ // Vary screen size
+ // TODO: Be smarter here: Pick a screen that is both as differently as possible
+ // from the current screen as well as also supported. So consider
+ // things like supported screens, targetSdk etc.
+ createScreenVariations(parent);
+
+ // Vary orientation
+ createStateVariation(chooser, parent);
+
+ // Vary render target
+ createRenderTargetVariation(chooser, parent);
+ }
+
+ // Also add in include-context previews, if any
+ addIncludedInPreviews();
+
+ // Make a placeholder preview for the current screen, in case we switch from it
+ RenderPreview preview = RenderPreview.create(this, parent);
+ mCanvas.setPreview(preview);
+
+ sortPreviewsByOrientation();
+ }
+
+ private void createRenderTargetVariation(ConfigurationChooser chooser, Configuration parent) {
+ /* This is disabled for now: need to load multiple versions of layoutlib.
+ When I did this, there seemed to be some drug interactions between
+ them, and I would end up with NPEs in layoutlib code which normally works.
+ VaryingConfiguration configuration =
+ VaryingConfiguration.create(chooser, parent);
+ configuration.setAlternatingTarget(true);
+ configuration.syncFolderConfig();
+ addPreview(RenderPreview.create(this, configuration));
+ */
+ }
+
+ private void createStateVariation(ConfigurationChooser chooser, Configuration parent) {
+ State currentState = parent.getDeviceState();
+ State nextState = parent.getNextDeviceState(currentState);
+ if (nextState != currentState) {
+ VaryingConfiguration configuration =
+ VaryingConfiguration.create(chooser, parent);
+ configuration.setAlternateDeviceState(true);
+ configuration.syncFolderConfig();
+ addPreview(RenderPreview.create(this, configuration));
+ }
+ }
+
+ private void createLocaleVariation(ConfigurationChooser chooser, Configuration parent) {
+ LocaleQualifier currentLanguage = parent.getLocale().qualifier;
+ for (Locale locale : chooser.getLocaleList()) {
+ LocaleQualifier qualifier = locale.qualifier;
+ if (!qualifier.getLanguage().equals(currentLanguage.getLanguage())) {
+ VaryingConfiguration configuration =
+ VaryingConfiguration.create(chooser, parent);
+ configuration.setAlternateLocale(true);
+ configuration.syncFolderConfig();
+ addPreview(RenderPreview.create(this, configuration));
+ break;
+ }
+ }
+ }
+
+ private void createScreenVariations(Configuration parent) {
+ ConfigurationChooser chooser = getChooser();
+ VaryingConfiguration configuration;
+
+ configuration = VaryingConfiguration.create(chooser, parent);
+ configuration.setVariation(0);
+ configuration.setAlternateDevice(true);
+ configuration.syncFolderConfig();
+ addPreview(RenderPreview.create(this, configuration));
+
+ configuration = VaryingConfiguration.create(chooser, parent);
+ configuration.setVariation(1);
+ configuration.setAlternateDevice(true);
+ configuration.syncFolderConfig();
+ addPreview(RenderPreview.create(this, configuration));
+ }
+
+ /**
+ * Returns the current mode as seen by this {@link RenderPreviewManager}.
+ * Note that it may not yet have been synced with the global mode kept in
+ * {@link AdtPrefs#getRenderPreviewMode()}.
+ *
+ * @return the current preview mode
+ */
+ @NonNull
+ public RenderPreviewMode getMode() {
+ return mMode;
+ }
+
+ /**
+ * Update the set of previews for the current mode
+ *
+ * @param force force a refresh even if the preview type has not changed
+ * @return true if the views were recomputed, false if the previews were
+ * already showing and the mode not changed
+ */
+ public boolean recomputePreviews(boolean force) {
+ RenderPreviewMode newMode = AdtPrefs.getPrefs().getRenderPreviewMode();
+ if (newMode == mMode && !force
+ && (mRevision == sRevision
+ || mMode == NONE
+ || mMode == CUSTOM)) {
+ return false;
+ }
+
+ RenderPreviewMode oldMode = mMode;
+ mMode = newMode;
+ mRevision = sRevision;
+
+ sScale = 1.0;
+ disposePreviews();
+
+ switch (mMode) {
+ case DEFAULT:
+ addDefaultPreviews();
+ break;
+ case INCLUDES:
+ addIncludedInPreviews();
+ break;
+ case LOCALES:
+ addLocalePreviews();
+ break;
+ case SCREENS:
+ addScreenSizePreviews();
+ break;
+ case VARIATIONS:
+ addVariationPreviews();
+ break;
+ case CUSTOM:
+ addManualPreviews();
+ break;
+ case NONE:
+ // Can't just set mNeedZoom because with no previews, the paint
+ // method does nothing
+ mCanvas.setFitScale(false /*onlyZoomOut*/, true /*allowZoomIn*/);
+ break;
+ default:
+ assert false : mMode;
+ }
+
+ // We schedule layout for the next redraw rather than process it here immediately;
+ // not only does this let us avoid doing work for windows where the tab is in the
+ // background, but when a file is opened we may not know the size of the canvas
+ // yet, and the layout methods need it in order to do a good job. By the time
+ // the canvas is painted, we have accurate bounds.
+ mNeedLayout = mNeedRender = true;
+ mCanvas.redraw();
+
+ if (oldMode != mMode && (oldMode == NONE || mMode == NONE)) {
+ // If entering or exiting preview mode: updating padding which is compressed
+ // only in preview mode.
+ mCanvas.getHorizontalTransform().refresh();
+ mCanvas.getVerticalTransform().refresh();
+ }
+
+ return true;
+ }
+
+ /**
+ * Sets the new render preview mode to use
+ *
+ * @param mode the new mode
+ */
+ public void selectMode(@NonNull RenderPreviewMode mode) {
+ if (mode != mMode) {
+ AdtPrefs.getPrefs().setPreviewMode(mode);
+ recomputePreviews(false);
+ }
+ }
+
+ /** Similar to {@link #addDefaultPreviews()} but for locales */
+ public void addLocalePreviews() {
+
+ ConfigurationChooser chooser = getChooser();
+ List<Locale> locales = chooser.getLocaleList();
+ Configuration parent = chooser.getConfiguration();
+
+ for (Locale locale : locales) {
+ if (!locale.hasLanguage() && !locale.hasRegion()) {
+ continue;
+ }
+ NestedConfiguration configuration = NestedConfiguration.create(chooser, parent);
+ configuration.setOverrideLocale(true);
+ configuration.setLocale(locale, false);
+
+ String displayName = ConfigurationChooser.getLocaleLabel(chooser, locale, false);
+ assert displayName != null; // it's never non null when locale is non null
+ configuration.setDisplayName(displayName);
+
+ addPreview(RenderPreview.create(this, configuration));
+ }
+
+ // Make a placeholder preview for the current screen, in case we switch from it
+ Configuration configuration = parent;
+ Locale locale = configuration.getLocale();
+ String label = ConfigurationChooser.getLocaleLabel(chooser, locale, false);
+ if (label == null) {
+ label = "default";
+ }
+ configuration.setDisplayName(label);
+ RenderPreview preview = RenderPreview.create(this, parent);
+ if (preview != null) {
+ mCanvas.setPreview(preview);
+ }
+
+ // No need to sort: they should all be identical
+ }
+
+ /** Similar to {@link #addDefaultPreviews()} but for screen sizes */
+ public void addScreenSizePreviews() {
+ ConfigurationChooser chooser = getChooser();
+ Collection<Device> devices = chooser.getDevices();
+ Configuration configuration = chooser.getConfiguration();
+ boolean canScaleNinePatch = configuration.supports(Capability.FIXED_SCALABLE_NINE_PATCH);
+
+ // Rearrange the devices a bit such that the most interesting devices bubble
+ // to the front
+ // 10" tablet, 7" tablet, reference phones, tiny phone, and in general the first
+ // version of each seen screen size
+ List<Device> sorted = new ArrayList<Device>(devices);
+ Set<ScreenSize> seenSizes = new HashSet<ScreenSize>();
+ State currentState = configuration.getDeviceState();
+ String currentStateName = currentState != null ? currentState.getName() : "";
+
+ for (int i = 0, n = sorted.size(); i < n; i++) {
+ Device device = sorted.get(i);
+ boolean interesting = false;
+
+ State state = device.getState(currentStateName);
+ if (state == null) {
+ state = device.getAllStates().get(0);
+ }
+
+ if (device.getName().startsWith("Nexus ") //$NON-NLS-1$
+ || device.getName().endsWith(" Nexus")) { //$NON-NLS-1$
+ // Not String#contains("Nexus") because that would also pick up all the generic
+ // entries ("3.7in WVGA (Nexus One)") so we'd have them duplicated
+ interesting = true;
+ }
+
+ FolderConfiguration c = DeviceConfigHelper.getFolderConfig(state);
+ if (c != null) {
+ ScreenSizeQualifier sizeQualifier = c.getScreenSizeQualifier();
+ if (sizeQualifier != null) {
+ ScreenSize size = sizeQualifier.getValue();
+ if (!seenSizes.contains(size)) {
+ seenSizes.add(size);
+ interesting = true;
+ }
+ }
+
+ // Omit LDPI, not really used anymore
+ DensityQualifier density = c.getDensityQualifier();
+ if (density != null) {
+ Density d = density.getValue();
+ if (d == Density.LOW) {
+ interesting = false;
+ }
+
+ if (!canScaleNinePatch && d == Density.TV) {
+ interesting = false;
+ }
+ }
+ }
+
+ if (interesting) {
+ NestedConfiguration screenConfig = NestedConfiguration.create(chooser,
+ configuration);
+ screenConfig.setOverrideDevice(true);
+ screenConfig.setDevice(device, true);
+ screenConfig.syncFolderConfig();
+ screenConfig.setDisplayName(ConfigurationChooser.getDeviceLabel(device, true));
+ addPreview(RenderPreview.create(this, screenConfig));
+ }
+ }
+
+ // Sorted by screen size, in decreasing order
+ sortPreviewsByScreenSize();
+ }
+
+ /**
+ * Previews this layout as included in other layouts
+ */
+ public void addIncludedInPreviews() {
+ ConfigurationChooser chooser = getChooser();
+ IProject project = chooser.getProject();
+ if (project == null) {
+ return;
+ }
+ IncludeFinder finder = IncludeFinder.get(project);
+
+ final List<Reference> includedBy = finder.getIncludedBy(chooser.getEditedFile());
+
+ if (includedBy == null || includedBy.isEmpty()) {
+ // TODO: Generate some useful defaults, such as including it in a ListView
+ // as the list item layout?
+ return;
+ }
+
+ for (final Reference reference : includedBy) {
+ String title = reference.getDisplayName();
+ Configuration config = Configuration.create(chooser.getConfiguration(),
+ reference.getFile());
+ RenderPreview preview = RenderPreview.create(this, config);
+ preview.setDisplayName(title);
+ preview.setIncludedWithin(reference);
+
+ addPreview(preview);
+ }
+
+ sortPreviewsByOrientation();
+ }
+
+ /**
+ * Previews this layout as included in other layouts
+ */
+ public void addVariationPreviews() {
+ ConfigurationChooser chooser = getChooser();
+
+ IFile file = chooser.getEditedFile();
+ List<IFile> variations = AdtUtils.getResourceVariations(file, false /*includeSelf*/);
+
+ // Sort by parent folder
+ Collections.sort(variations, new Comparator<IFile>() {
+ @Override
+ public int compare(IFile file1, IFile file2) {
+ return file1.getParent().getName().compareTo(file2.getParent().getName());
+ }
+ });
+
+ Configuration currentConfig = chooser.getConfiguration();
+
+ for (IFile variation : variations) {
+ String title = variation.getParent().getName();
+ Configuration config = Configuration.create(chooser.getConfiguration(), variation);
+ config.setTheme(currentConfig.getTheme());
+ config.setActivity(currentConfig.getActivity());
+ RenderPreview preview = RenderPreview.create(this, config);
+ preview.setDisplayName(title);
+ preview.setAlternateInput(variation);
+
+ addPreview(preview);
+ }
+
+ sortPreviewsByOrientation();
+ }
+
+ /**
+ * Previews this layout using a custom configured set of layouts
+ */
+ public void addManualPreviews() {
+ if (mManualList == null) {
+ loadList();
+ } else {
+ mPreviews = mManualList.createPreviews(mCanvas);
+ }
+ }
+
+ private void loadList() {
+ IProject project = getChooser().getProject();
+ if (project == null) {
+ return;
+ }
+
+ if (mManualList == null) {
+ mManualList = RenderPreviewList.get(project);
+ }
+
+ try {
+ mManualList.load(getChooser().getDevices());
+ mPreviews = mManualList.createPreviews(mCanvas);
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ private void saveList() {
+ if (mManualList != null) {
+ try {
+ mManualList.save();
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ }
+
+ void rename(ConfigurationDescription description, String newName) {
+ IProject project = getChooser().getProject();
+ if (project == null) {
+ return;
+ }
+
+ if (mManualList == null) {
+ mManualList = RenderPreviewList.get(project);
+ }
+ description.displayName = newName;
+ saveList();
+ }
+
+
+ /**
+ * Notifies that the main configuration has changed.
+ *
+ * @param flags the change flags, a bitmask corresponding to the
+ * {@code CHANGE_} constants in {@link ConfigurationClient}
+ */
+ public void configurationChanged(int flags) {
+ // Similar to renderPreviews, but only acts on incomplete previews
+ if (hasPreviews()) {
+ // Do zoomed images first
+ beginRenderScheduling();
+ for (RenderPreview preview : mPreviews) {
+ if (preview.getScale() > 1.2) {
+ preview.configurationChanged(flags);
+ }
+ }
+ for (RenderPreview preview : mPreviews) {
+ if (preview.getScale() <= 1.2) {
+ preview.configurationChanged(flags);
+ }
+ }
+ RenderPreview preview = mCanvas.getPreview();
+ if (preview != null) {
+ preview.configurationChanged(flags);
+ preview.dispose();
+ }
+ mNeedLayout = true;
+ mCanvas.redraw();
+ }
+ }
+
+ /** Updates the configuration preview thumbnails */
+ public void renderPreviews() {
+ if (hasPreviews()) {
+ beginRenderScheduling();
+
+ // Process in visual order
+ ArrayList<RenderPreview> visualOrder = new ArrayList<RenderPreview>(mPreviews);
+ Collections.sort(visualOrder, RenderPreview.VISUAL_ORDER);
+
+ // Do zoomed images first
+ for (RenderPreview preview : visualOrder) {
+ if (preview.getScale() > 1.2 && preview.isVisible()) {
+ scheduleRender(preview);
+ }
+ }
+ // Non-zoomed images
+ for (RenderPreview preview : visualOrder) {
+ if (preview.getScale() <= 1.2 && preview.isVisible()) {
+ scheduleRender(preview);
+ }
+ }
+ }
+
+ mNeedRender = false;
+ }
+
+ private int mPendingRenderCount;
+
+ /**
+ * Reset rendering scheduling. The next render request will be scheduled
+ * after a single delay unit.
+ */
+ public void beginRenderScheduling() {
+ mPendingRenderCount = 0;
+ }
+
+ /**
+ * Schedule rendering the given preview. Each successive call will add an additional
+ * delay unit to the schedule from the previous {@link #scheduleRender(RenderPreview)}
+ * call, until {@link #beginRenderScheduling()} is called again.
+ *
+ * @param preview the preview to render
+ */
+ public void scheduleRender(@NonNull RenderPreview preview) {
+ mPendingRenderCount++;
+ preview.render(mPendingRenderCount * RENDER_DELAY);
+ }
+
+ /**
+ * Switch to the given configuration preview
+ *
+ * @param preview the preview to switch to
+ */
+ public void switchTo(@NonNull RenderPreview preview) {
+ IFile input = preview.getAlternateInput();
+ if (input != null) {
+ IWorkbenchPartSite site = mCanvas.getEditorDelegate().getEditor().getSite();
+ try {
+ // This switches to the given file, but the file might not have
+ // an identical configuration to what was shown in the preview.
+ // For example, while viewing a 10" layout-xlarge file, it might
+ // show a preview for a 5" version tied to the default layout. If
+ // you click on it, it will open the default layout file, but it might
+ // be using a different screen size; any of those that match the
+ // default layout, say a 3.8".
+ //
+ // Thus, we need to also perform a screen size sync first
+ Configuration configuration = preview.getConfiguration();
+ boolean setSize = false;
+ if (configuration instanceof NestedConfiguration) {
+ NestedConfiguration nestedConfig = (NestedConfiguration) configuration;
+ setSize = nestedConfig.isOverridingDevice();
+ if (configuration instanceof VaryingConfiguration) {
+ VaryingConfiguration c = (VaryingConfiguration) configuration;
+ setSize |= c.isAlternatingDevice();
+ }
+
+ if (setSize) {
+ ConfigurationChooser chooser = getChooser();
+ IFile editedFile = chooser.getEditedFile();
+ if (editedFile != null) {
+ chooser.syncToVariations(CFG_DEVICE|CFG_DEVICE_STATE,
+ editedFile, configuration, false, false);
+ }
+ }
+ }
+
+ IDE.openEditor(site.getWorkbenchWindow().getActivePage(), input,
+ CommonXmlEditor.ID);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, null);
+ }
+ return;
+ }
+
+ GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ ConfigurationChooser chooser = editor.getConfigurationChooser();
+
+ Configuration originalConfiguration = chooser.getConfiguration();
+
+ // The new configuration is the configuration which will become the configuration
+ // in the layout editor's chooser
+ Configuration previewConfiguration = preview.getConfiguration();
+ Configuration newConfiguration = previewConfiguration;
+ if (newConfiguration instanceof NestedConfiguration) {
+ // Should never use a complementing configuration for the main
+ // rendering's configuration; instead, create a new configuration
+ // with a snapshot of the configuration's current values
+ newConfiguration = Configuration.copy(previewConfiguration);
+
+ // Remap all the previews to be parented to this new copy instead
+ // of the old one (which is no longer controlled by the chooser)
+ for (RenderPreview p : mPreviews) {
+ Configuration configuration = p.getConfiguration();
+ if (configuration instanceof NestedConfiguration) {
+ NestedConfiguration nested = (NestedConfiguration) configuration;
+ nested.setParent(newConfiguration);
+ }
+ }
+ }
+
+ // Make a preview for the configuration which *was* showing in the
+ // chooser up until this point:
+ RenderPreview newPreview = mCanvas.getPreview();
+ if (newPreview == null) {
+ newPreview = RenderPreview.create(this, originalConfiguration);
+ }
+
+ // Update its configuration such that it is complementing or inheriting
+ // from the new chosen configuration
+ if (previewConfiguration instanceof VaryingConfiguration) {
+ VaryingConfiguration varying = VaryingConfiguration.create(
+ (VaryingConfiguration) previewConfiguration,
+ newConfiguration);
+ varying.updateDisplayName();
+ originalConfiguration = varying;
+ newPreview.setConfiguration(originalConfiguration);
+ } else if (previewConfiguration instanceof NestedConfiguration) {
+ NestedConfiguration nested = NestedConfiguration.create(
+ (NestedConfiguration) previewConfiguration,
+ originalConfiguration,
+ newConfiguration);
+ nested.setDisplayName(nested.computeDisplayName());
+ originalConfiguration = nested;
+ newPreview.setConfiguration(originalConfiguration);
+ }
+
+ // Replace clicked preview with preview of the formerly edited main configuration
+ // This doesn't work yet because the image overlay has had its image
+ // replaced by the configuration previews! I should make a list of them
+ //newPreview.setFullImage(mImageOverlay.getAwtImage());
+ for (int i = 0, n = mPreviews.size(); i < n; i++) {
+ if (preview == mPreviews.get(i)) {
+ mPreviews.set(i, newPreview);
+ break;
+ }
+ }
+
+ // Stash the corresponding preview (not active) on the canvas so we can
+ // retrieve it if clicking to some other preview later
+ mCanvas.setPreview(preview);
+ preview.setVisible(false);
+
+ // Switch to the configuration from the clicked preview (though it's
+ // most likely a copy, see above)
+ chooser.setConfiguration(newConfiguration);
+ editor.changed(MASK_ALL);
+
+ // Scroll to the top again, if necessary
+ mCanvas.getVerticalBar().setSelection(mCanvas.getVerticalBar().getMinimum());
+
+ mNeedLayout = mNeedZoom = true;
+ mCanvas.redraw();
+ mAnimation = new SwapAnimation(preview, newPreview);
+ }
+
+ /**
+ * Gets the preview at the given location, or null if none. This is
+ * currently deeply tied to where things are painted in onPaint().
+ */
+ RenderPreview getPreview(ControlPoint mousePos) {
+ if (hasPreviews()) {
+ int rootX = getX();
+ if (mousePos.x < rootX) {
+ return null;
+ }
+ int rootY = getY();
+
+ for (RenderPreview preview : mPreviews) {
+ int x = rootX + preview.getX();
+ int y = rootY + preview.getY();
+ if (mousePos.x >= x && mousePos.x <= x + preview.getWidth()) {
+ if (mousePos.y >= y && mousePos.y <= y + preview.getHeight()) {
+ return preview;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private int getX() {
+ return mHScale.translate(0);
+ }
+
+ private int getY() {
+ return mVScale.translate(0);
+ }
+
+ private int getZoomX() {
+ Rectangle clientArea = mCanvas.getClientArea();
+ int x = clientArea.x + clientArea.width - ZOOM_ICON_WIDTH;
+ if (x < mHScale.getScaledImgSize() + PREVIEW_HGAP) {
+ // No visible previews because the main image is zoomed too far
+ return -1;
+ }
+
+ return x - 6;
+ }
+
+ private int getZoomY() {
+ Rectangle clientArea = mCanvas.getClientArea();
+ return clientArea.y + 5;
+ }
+
+ /**
+ * Returns the height of the layout
+ *
+ * @return the height
+ */
+ public int getHeight() {
+ return mLayoutHeight;
+ }
+
+ /**
+ * Notifies that preview manager that the mouse cursor has moved to the
+ * given control position within the layout canvas
+ *
+ * @param mousePos the mouse position, relative to the layout canvas
+ */
+ public void moved(ControlPoint mousePos) {
+ RenderPreview hovered = getPreview(mousePos);
+ if (hovered != mActivePreview) {
+ if (mActivePreview != null) {
+ mActivePreview.setActive(false);
+ }
+ mActivePreview = hovered;
+ if (mActivePreview != null) {
+ mActivePreview.setActive(true);
+ }
+ mCanvas.redraw();
+ }
+ }
+
+ /**
+ * Notifies that preview manager that the mouse cursor has entered the layout canvas
+ *
+ * @param mousePos the mouse position, relative to the layout canvas
+ */
+ public void enter(ControlPoint mousePos) {
+ moved(mousePos);
+ }
+
+ /**
+ * Notifies that preview manager that the mouse cursor has exited the layout canvas
+ *
+ * @param mousePos the mouse position, relative to the layout canvas
+ */
+ public void exit(ControlPoint mousePos) {
+ if (mActivePreview != null) {
+ mActivePreview.setActive(false);
+ }
+ mActivePreview = null;
+ mCanvas.redraw();
+ }
+
+ /**
+ * Process a mouse click, and return true if it was handled by this manager
+ * (e.g. the click was on a preview)
+ *
+ * @param mousePos the mouse position where the click occurred
+ * @return true if the click occurred over a preview and was handled, false otherwise
+ */
+ public boolean click(ControlPoint mousePos) {
+ // Clicked zoom?
+ int x = getZoomX();
+ if (x > 0) {
+ if (mousePos.x >= x && mousePos.x <= x + ZOOM_ICON_WIDTH) {
+ int y = getZoomY();
+ if (mousePos.y >= y && mousePos.y <= y + 4 * ZOOM_ICON_HEIGHT) {
+ if (mousePos.y < y + ZOOM_ICON_HEIGHT) {
+ zoomIn();
+ } else if (mousePos.y < y + 2 * ZOOM_ICON_HEIGHT) {
+ zoomOut();
+ } else if (mousePos.y < y + 3 * ZOOM_ICON_HEIGHT) {
+ zoomReset();
+ } else {
+ selectMode(NONE);
+ }
+ return true;
+ }
+ }
+ }
+
+ RenderPreview preview = getPreview(mousePos);
+ if (preview != null) {
+ boolean handled = preview.click(mousePos.x - getX() - preview.getX(),
+ mousePos.y - getY() - preview.getY());
+ if (handled) {
+ // In case layout was performed, there could be a new preview
+ // under this coordinate now, so make sure it's hover etc
+ // shows up
+ moved(mousePos);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if there are thumbnail previews
+ *
+ * @return true if thumbnails are being shown
+ */
+ public boolean hasPreviews() {
+ return mPreviews != null && !mPreviews.isEmpty();
+ }
+
+
+ private void sortPreviewsByScreenSize() {
+ if (mPreviews != null) {
+ Collections.sort(mPreviews, new Comparator<RenderPreview>() {
+ @Override
+ public int compare(RenderPreview preview1, RenderPreview preview2) {
+ Configuration config1 = preview1.getConfiguration();
+ Configuration config2 = preview2.getConfiguration();
+ Device device1 = config1.getDevice();
+ Device device2 = config1.getDevice();
+ if (device1 != null && device2 != null) {
+ Screen screen1 = device1.getDefaultHardware().getScreen();
+ Screen screen2 = device2.getDefaultHardware().getScreen();
+ if (screen1 != null && screen2 != null) {
+ double delta = screen1.getDiagonalLength()
+ - screen2.getDiagonalLength();
+ if (delta != 0.0) {
+ return (int) Math.signum(delta);
+ } else {
+ if (screen1.getPixelDensity() != screen2.getPixelDensity()) {
+ return screen1.getPixelDensity().compareTo(
+ screen2.getPixelDensity());
+ }
+ }
+ }
+
+ }
+ State state1 = config1.getDeviceState();
+ State state2 = config2.getDeviceState();
+ if (state1 != state2 && state1 != null && state2 != null) {
+ return state1.getName().compareTo(state2.getName());
+ }
+
+ return preview1.getDisplayName().compareTo(preview2.getDisplayName());
+ }
+ });
+ }
+ }
+
+ private void sortPreviewsByOrientation() {
+ if (mPreviews != null) {
+ Collections.sort(mPreviews, new Comparator<RenderPreview>() {
+ @Override
+ public int compare(RenderPreview preview1, RenderPreview preview2) {
+ Configuration config1 = preview1.getConfiguration();
+ Configuration config2 = preview2.getConfiguration();
+ State state1 = config1.getDeviceState();
+ State state2 = config2.getDeviceState();
+ if (state1 != state2 && state1 != null && state2 != null) {
+ return state1.getName().compareTo(state2.getName());
+ }
+
+ return preview1.getDisplayName().compareTo(preview2.getDisplayName());
+ }
+ });
+ }
+ }
+
+ /**
+ * Vertical scrollbar listener which updates render previews which are not
+ * visible and triggers a redraw
+ */
+ private class ScrollBarListener implements SelectionListener {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mPreviews == null) {
+ return;
+ }
+
+ ScrollBar bar = mCanvas.getVerticalBar();
+ int selection = bar.getSelection();
+ int thumb = bar.getThumb();
+ int maxY = selection + thumb;
+ beginRenderScheduling();
+ for (RenderPreview preview : mPreviews) {
+ if (!preview.isVisible() && preview.getY() <= maxY) {
+ preview.setVisible(true);
+ }
+ }
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+ }
+
+ /** Animation overlay shown briefly after swapping two previews */
+ private class SwapAnimation implements Runnable {
+ private long begin;
+ private long end;
+ private static final long DURATION = 400; // ms
+ private Rect initialRect1;
+ private Rect targetRect1;
+ private Rect initialRect2;
+ private Rect targetRect2;
+ private RenderPreview preview;
+
+ SwapAnimation(RenderPreview preview1, RenderPreview preview2) {
+ begin = System.currentTimeMillis();
+ end = begin + DURATION;
+
+ initialRect1 = new Rect(preview1.getX(), preview1.getY(),
+ preview1.getWidth(), preview1.getHeight());
+
+ CanvasTransform hi = mCanvas.getHorizontalTransform();
+ CanvasTransform vi = mCanvas.getVerticalTransform();
+ initialRect2 = new Rect(hi.translate(0), vi.translate(0),
+ hi.getScaledImgSize(), vi.getScaledImgSize());
+ preview = preview2;
+ }
+
+ void tick(GC gc) {
+ long now = System.currentTimeMillis();
+ if (now > end || mCanvas.isDisposed()) {
+ mAnimation = null;
+ return;
+ }
+
+ CanvasTransform hi = mCanvas.getHorizontalTransform();
+ CanvasTransform vi = mCanvas.getVerticalTransform();
+ if (targetRect1 == null) {
+ targetRect1 = new Rect(hi.translate(0), vi.translate(0),
+ hi.getScaledImgSize(), vi.getScaledImgSize());
+ }
+ double portion = (now - begin) / (double) DURATION;
+ Rect rect1 = new Rect(
+ (int) (portion * (targetRect1.x - initialRect1.x) + initialRect1.x),
+ (int) (portion * (targetRect1.y - initialRect1.y) + initialRect1.y),
+ (int) (portion * (targetRect1.w - initialRect1.w) + initialRect1.w),
+ (int) (portion * (targetRect1.h - initialRect1.h) + initialRect1.h));
+
+ if (targetRect2 == null) {
+ targetRect2 = new Rect(preview.getX(), preview.getY(),
+ preview.getWidth(), preview.getHeight());
+ }
+ portion = (now - begin) / (double) DURATION;
+ Rect rect2 = new Rect(
+ (int) (portion * (targetRect2.x - initialRect2.x) + initialRect2.x),
+ (int) (portion * (targetRect2.y - initialRect2.y) + initialRect2.y),
+ (int) (portion * (targetRect2.w - initialRect2.w) + initialRect2.w),
+ (int) (portion * (targetRect2.h - initialRect2.h) + initialRect2.h));
+
+ gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY));
+ gc.drawRectangle(rect1.x, rect1.y, rect1.w, rect1.h);
+ gc.drawRectangle(rect2.x, rect2.y, rect2.w, rect2.h);
+
+ mCanvas.getDisplay().timerExec(5, this);
+ }
+
+ @Override
+ public void run() {
+ mCanvas.redraw();
+ }
+ }
+
+ /**
+ * Notifies the {@linkplain RenderPreviewManager} that the configuration used
+ * in the main chooser has been changed. This may require updating parent references
+ * in the preview configurations inheriting from it.
+ *
+ * @param oldConfiguration the previous configuration
+ * @param newConfiguration the new configuration in the chooser
+ */
+ public void updateChooserConfig(
+ @NonNull Configuration oldConfiguration,
+ @NonNull Configuration newConfiguration) {
+ if (hasPreviews()) {
+ for (RenderPreview preview : mPreviews) {
+ Configuration configuration = preview.getConfiguration();
+ if (configuration instanceof NestedConfiguration) {
+ NestedConfiguration nestedConfig = (NestedConfiguration) configuration;
+ if (nestedConfig.getParent() == oldConfiguration) {
+ nestedConfig.setParent(newConfiguration);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewMode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewMode.java
new file mode 100644
index 000000000..0f06d7f8a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewMode.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2012 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;
+
+/**
+ * The {@linkplain RenderPreviewMode} records what type of configurations to
+ * render in the layout editor
+ */
+public enum RenderPreviewMode {
+ /** Generate a set of default previews with maximum variation */
+ DEFAULT,
+
+ /** Preview all the locales */
+ LOCALES,
+
+ /** Preview all the screen sizes */
+ SCREENS,
+
+ /** Preview layout as included in other layouts */
+ INCLUDES,
+
+ /** Preview all the variations of this layout */
+ VARIATIONS,
+
+ /** Show a manually configured set of previews */
+ CUSTOM,
+
+ /** No previews */
+ NONE;
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java
new file mode 100644
index 000000000..3b9e2fc0f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java
@@ -0,0 +1,668 @@
+/*
+ * Copyright (C) 2011 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 static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
+
+import com.android.annotations.NonNull;
+import com.android.ide.common.api.IClientRulesEngine;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.rendering.HardwareConfigHelper;
+import com.android.ide.common.rendering.LayoutLibrary;
+import com.android.ide.common.rendering.RenderSecurityManager;
+import com.android.ide.common.rendering.api.AssetRepository;
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.rendering.api.DrawableParams;
+import com.android.ide.common.rendering.api.HardwareConfig;
+import com.android.ide.common.rendering.api.IImageFactory;
+import com.android.ide.common.rendering.api.ILayoutPullParser;
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.ide.common.rendering.api.Result;
+import com.android.ide.common.rendering.api.SessionParams;
+import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.common.resources.ResourceResolver;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.layout.ContextPullParser;
+import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback;
+import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Locale;
+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.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo.ActivityAttributes;
+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.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.devices.Device;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+
+import org.eclipse.core.resources.IProject;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.awt.Toolkit;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The {@link RenderService} provides rendering and layout information for
+ * Android layouts. This is a wrapper around the layout library.
+ */
+public class RenderService {
+ private static final Object RENDERING_LOCK = new Object();
+
+ /** Reference to the file being edited. Can also be used to access the {@link IProject}. */
+ private final GraphicalEditorPart mEditor;
+
+ // The following fields are inferred from the editor and not customizable by the
+ // client of the render service:
+
+ private final IProject mProject;
+ private final ProjectCallback mProjectCallback;
+ private final ResourceResolver mResourceResolver;
+ private final int mMinSdkVersion;
+ private final int mTargetSdkVersion;
+ private final LayoutLibrary mLayoutLib;
+ private final IImageFactory mImageFactory;
+ private final HardwareConfigHelper mHardwareConfigHelper;
+ private final Locale mLocale;
+
+ // The following fields are optional or configurable using the various chained
+ // setters:
+
+ private UiDocumentNode mModel;
+ private Reference mIncludedWithin;
+ private RenderingMode mRenderingMode = RenderingMode.NORMAL;
+ private LayoutLog mLogger;
+ private Integer mOverrideBgColor;
+ private boolean mShowDecorations = true;
+ private Set<UiElementNode> mExpandNodes = Collections.<UiElementNode>emptySet();
+ private final Object mCredential;
+
+ /** Use the {@link #create} factory instead */
+ private RenderService(GraphicalEditorPart editor, Object credential) {
+ mEditor = editor;
+ mCredential = credential;
+
+ mProject = editor.getProject();
+ LayoutCanvas canvas = editor.getCanvasControl();
+ mImageFactory = canvas.getImageOverlay();
+ ConfigurationChooser chooser = editor.getConfigurationChooser();
+ Configuration config = chooser.getConfiguration();
+ FolderConfiguration folderConfig = config.getFullConfig();
+
+ Device device = config.getDevice();
+ assert device != null; // Should only attempt render with configuration that has device
+ mHardwareConfigHelper = new HardwareConfigHelper(device);
+ mHardwareConfigHelper.setOrientation(
+ folderConfig.getScreenOrientationQualifier().getValue());
+
+ mLayoutLib = editor.getReadyLayoutLib(true /*displayError*/);
+ mResourceResolver = editor.getResourceResolver();
+ mProjectCallback = editor.getProjectCallback(true /*reset*/, mLayoutLib);
+ mMinSdkVersion = editor.getMinSdkVersion();
+ mTargetSdkVersion = editor.getTargetSdkVersion();
+ mLocale = config.getLocale();
+ }
+
+ private RenderService(GraphicalEditorPart editor,
+ Configuration configuration, ResourceResolver resourceResolver,
+ Object credential) {
+ mEditor = editor;
+ mCredential = credential;
+
+ mProject = editor.getProject();
+ LayoutCanvas canvas = editor.getCanvasControl();
+ mImageFactory = canvas.getImageOverlay();
+ FolderConfiguration folderConfig = configuration.getFullConfig();
+
+ Device device = configuration.getDevice();
+ assert device != null;
+ mHardwareConfigHelper = new HardwareConfigHelper(device);
+ mHardwareConfigHelper.setOrientation(
+ folderConfig.getScreenOrientationQualifier().getValue());
+
+ mLayoutLib = editor.getReadyLayoutLib(true /*displayError*/);
+ mResourceResolver = resourceResolver != null ? resourceResolver : editor.getResourceResolver();
+ mProjectCallback = editor.getProjectCallback(true /*reset*/, mLayoutLib);
+ mMinSdkVersion = editor.getMinSdkVersion();
+ mTargetSdkVersion = editor.getTargetSdkVersion();
+ mLocale = configuration.getLocale();
+ }
+
+ private RenderSecurityManager createSecurityManager() {
+ String projectPath = null;
+ String sdkPath = null;
+ if (RenderSecurityManager.RESTRICT_READS) {
+ projectPath = AdtUtils.getAbsolutePath(mProject).toFile().getPath();
+ Sdk sdk = Sdk.getCurrent();
+ sdkPath = sdk != null ? sdk.getSdkOsLocation() : null;
+ }
+ RenderSecurityManager securityManager = new RenderSecurityManager(sdkPath, projectPath);
+ securityManager.setLogger(AdtPlugin.getDefault());
+
+ // Make sure this is initialized before we attempt to use it from layoutlib
+ Toolkit.getDefaultToolkit();
+
+ return securityManager;
+ }
+
+ /**
+ * Returns true if this configuration supports the given rendering
+ * capability
+ *
+ * @param target the target to look up the layout library for
+ * @param capability the capability to check
+ * @return true if the capability is supported
+ */
+ public static boolean supports(
+ @NonNull IAndroidTarget target,
+ @NonNull Capability capability) {
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ AndroidTargetData targetData = sdk.getTargetData(target);
+ if (targetData != null) {
+ LayoutLibrary layoutLib = targetData.getLayoutLibrary();
+ if (layoutLib != null) {
+ return layoutLib.supports(capability);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Creates a new {@link RenderService} associated with the given editor.
+ *
+ * @param editor the editor to provide configuration data such as the render target
+ * @return a {@link RenderService} which can perform rendering services
+ */
+ public static RenderService create(GraphicalEditorPart editor) {
+ // Delegate to editor such that it can pass its credential to the service
+ return editor.createRenderService();
+ }
+
+ /**
+ * Creates a new {@link RenderService} associated with the given editor.
+ *
+ * @param editor the editor to provide configuration data such as the render target
+ * @param credential the sandbox credential
+ * @return a {@link RenderService} which can perform rendering services
+ */
+ @NonNull
+ public static RenderService create(GraphicalEditorPart editor, Object credential) {
+ return new RenderService(editor, credential);
+ }
+
+ /**
+ * Creates a new {@link RenderService} associated with the given editor.
+ *
+ * @param editor the editor to provide configuration data such as the render target
+ * @param configuration the configuration to use (and fallback to editor for the rest)
+ * @param resolver a resource resolver to use to look up resources
+ * @return a {@link RenderService} which can perform rendering services
+ */
+ public static RenderService create(GraphicalEditorPart editor,
+ Configuration configuration, ResourceResolver resolver) {
+ // Delegate to editor such that it can pass its credential to the service
+ return editor.createRenderService(configuration, resolver);
+ }
+
+ /**
+ * Creates a new {@link RenderService} associated with the given editor.
+ *
+ * @param editor the editor to provide configuration data such as the render target
+ * @param configuration the configuration to use (and fallback to editor for the rest)
+ * @param resolver a resource resolver to use to look up resources
+ * @param credential the sandbox credential
+ * @return a {@link RenderService} which can perform rendering services
+ */
+ public static RenderService create(GraphicalEditorPart editor,
+ Configuration configuration, ResourceResolver resolver, Object credential) {
+ return new RenderService(editor, configuration, resolver, credential);
+ }
+
+ /**
+ * Renders the given model, using this editor's theme and screen settings, and returns
+ * the result as a {@link RenderSession}.
+ *
+ * @param model the model to be rendered, which can be different than the editor's own
+ * {@link #getModel()}.
+ * @param width the width to use for the layout, or -1 to use the width of the screen
+ * associated with this editor
+ * @param height the height to use for the layout, or -1 to use the height of the screen
+ * associated with this editor
+ * @param explodeNodes a set of nodes to explode, or null for none
+ * @param overrideBgColor If non-null, use the given color as a background to render over
+ * rather than the normal background requested by the theme
+ * @param noDecor If true, don't draw window decorations like the system bar
+ * @param logger a logger where rendering errors are reported
+ * @param renderingMode the {@link RenderingMode} to use for rendering
+ * @return the resulting rendered image wrapped in an {@link RenderSession}
+ */
+
+ /**
+ * Sets the {@link LayoutLog} to be used during rendering. If none is specified, a
+ * silent logger will be used.
+ *
+ * @param logger the log to be used
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setLog(LayoutLog logger) {
+ mLogger = logger;
+ return this;
+ }
+
+ /**
+ * Sets the model to be rendered, which can be different than the editor's own
+ * {@link GraphicalEditorPart#getModel()}.
+ *
+ * @param model the model to be rendered
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setModel(UiDocumentNode model) {
+ mModel = model;
+ return this;
+ }
+
+ /**
+ * Overrides the width and height to be used during rendering (which might be adjusted if
+ * the {@link #setRenderingMode(RenderingMode)} is {@link RenderingMode#FULL_EXPAND}.
+ *
+ * A value of -1 will make the rendering use the normal width and height coming from the
+ * {@link Configuration#getDevice()} object.
+ *
+ * @param overrideRenderWidth the width in pixels of the layout to be rendered
+ * @param overrideRenderHeight the height in pixels of the layout to be rendered
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setOverrideRenderSize(int overrideRenderWidth, int overrideRenderHeight) {
+ mHardwareConfigHelper.setOverrideRenderSize(overrideRenderWidth, overrideRenderHeight);
+ return this;
+ }
+
+ /**
+ * Sets the max width and height to be used during rendering (which might be adjusted if
+ * the {@link #setRenderingMode(RenderingMode)} is {@link RenderingMode#FULL_EXPAND}.
+ *
+ * A value of -1 will make the rendering use the normal width and height coming from the
+ * {@link Configuration#getDevice()} object.
+ *
+ * @param maxRenderWidth the max width in pixels of the layout to be rendered
+ * @param maxRenderHeight the max height in pixels of the layout to be rendered
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setMaxRenderSize(int maxRenderWidth, int maxRenderHeight) {
+ mHardwareConfigHelper.setMaxRenderSize(maxRenderWidth, maxRenderHeight);
+ return this;
+ }
+
+ /**
+ * Sets the {@link RenderingMode} to be used during rendering. If none is specified,
+ * the default is {@link RenderingMode#NORMAL}.
+ *
+ * @param renderingMode the rendering mode to be used
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setRenderingMode(RenderingMode renderingMode) {
+ mRenderingMode = renderingMode;
+ return this;
+ }
+
+ /**
+ * Sets the overriding background color to be used, if any. The color should be a
+ * bitmask of AARRGGBB. The default is null.
+ *
+ * @param overrideBgColor the overriding background color to be used in the rendering,
+ * in the form of a AARRGGBB bitmask, or null to use no custom background.
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setOverrideBgColor(Integer overrideBgColor) {
+ mOverrideBgColor = overrideBgColor;
+ return this;
+ }
+
+ /**
+ * Sets whether the rendering should include decorations such as a system bar, an
+ * application bar etc depending on the SDK target and theme. The default is true.
+ *
+ * @param showDecorations true if the rendering should include system bars etc.
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setDecorations(boolean showDecorations) {
+ mShowDecorations = showDecorations;
+ return this;
+ }
+
+ /**
+ * Sets the nodes to expand during rendering. These will be padded with approximately
+ * 20 pixels and also highlighted by the {@link EmptyViewsOverlay}. The default is an
+ * empty collection.
+ *
+ * @param nodesToExpand the nodes to be expanded
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setNodesToExpand(Set<UiElementNode> nodesToExpand) {
+ mExpandNodes = nodesToExpand;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Reference} to an outer layout that this layout should be rendered
+ * within. The outer layout <b>must</b> contain an include tag which points to this
+ * layout. The default is null.
+ *
+ * @param includedWithin a reference to an outer layout to render this layout within
+ * @return this (such that chains of setters can be stringed together)
+ */
+ public RenderService setIncludedWithin(Reference includedWithin) {
+ mIncludedWithin = includedWithin;
+ return this;
+ }
+
+ /** Initializes any remaining optional fields after all setters have been called */
+ private void finishConfiguration() {
+ if (mLogger == null) {
+ // Silent logging
+ mLogger = new LayoutLog();
+ }
+ }
+
+ /**
+ * Renders the model and returns the result as a {@link RenderSession}.
+ * @return the {@link RenderSession} resulting from rendering the current model
+ */
+ public RenderSession createRenderSession() {
+ assert mModel != null : "Incomplete service config";
+ finishConfiguration();
+
+ if (mResourceResolver == null) {
+ // Abort the rendering if the resources are not found.
+ return null;
+ }
+
+ HardwareConfig hardwareConfig = mHardwareConfigHelper.getConfig();
+
+ UiElementPullParser modelParser = new UiElementPullParser(mModel,
+ false, mExpandNodes, hardwareConfig.getDensity(), mProject);
+ ILayoutPullParser topParser = modelParser;
+
+ // Code to support editing included layout
+ // first reset the layout parser just in case.
+ mProjectCallback.setLayoutParser(null, null);
+
+ if (mIncludedWithin != null) {
+ // Outer layout name:
+ String contextLayoutName = mIncludedWithin.getName();
+
+ // Find the layout file.
+ ResourceValue contextLayout = mResourceResolver.findResValue(
+ LAYOUT_RESOURCE_PREFIX + contextLayoutName, false /* forceFrameworkOnly*/);
+ if (contextLayout != null) {
+ File layoutFile = new File(contextLayout.getValue());
+ if (layoutFile.isFile()) {
+ try {
+ // Get the name of the layout actually being edited, without the extension
+ // as it's what IXmlPullParser.getParser(String) will receive.
+ String queryLayoutName = mEditor.getLayoutResourceName();
+ mProjectCallback.setLayoutParser(queryLayoutName, modelParser);
+ topParser = new ContextPullParser(mProjectCallback, layoutFile);
+ topParser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ String xmlText = Files.toString(layoutFile, Charsets.UTF_8);
+ topParser.setInput(new StringReader(xmlText));
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ } catch (XmlPullParserException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ }
+ }
+
+ SessionParams params = new SessionParams(
+ topParser,
+ mRenderingMode,
+ mProject /* projectKey */,
+ hardwareConfig,
+ mResourceResolver,
+ mProjectCallback,
+ mMinSdkVersion,
+ mTargetSdkVersion,
+ mLogger);
+
+ // Request margin and baseline information.
+ // TODO: Be smarter about setting this; start without it, and on the first request
+ // for an extended view info, re-render in the same session, and then set a flag
+ // which will cause this to create extended view info each time from then on in the
+ // same session
+ params.setExtendedViewInfoMode(true);
+
+ params.setLocale(mLocale.toLocaleId());
+ params.setAssetRepository(new AssetRepository());
+
+ ManifestInfo manifestInfo = ManifestInfo.get(mProject);
+ try {
+ params.setRtlSupport(manifestInfo.isRtlSupported());
+ } catch (Exception e) {
+ // ignore.
+ }
+ if (!mShowDecorations) {
+ params.setForceNoDecor();
+ } else {
+ try {
+ params.setAppLabel(manifestInfo.getApplicationLabel());
+ params.setAppIcon(manifestInfo.getApplicationIcon());
+ String activity = mEditor.getConfigurationChooser().getConfiguration().getActivity();
+ if (activity != null) {
+ ActivityAttributes info = manifestInfo.getActivityAttributes(activity);
+ if (info != null) {
+ if (info.getLabel() != null) {
+ params.setAppLabel(info.getLabel());
+ }
+ if (info.getIcon() != null) {
+ params.setAppIcon(info.getIcon());
+ }
+ }
+ }
+ } catch (Exception e) {
+ // ignore.
+ }
+ }
+
+ if (mOverrideBgColor != null) {
+ params.setOverrideBgColor(mOverrideBgColor.intValue());
+ }
+
+ // set the Image Overlay as the image factory.
+ params.setImageFactory(mImageFactory);
+
+ mProjectCallback.setLogger(mLogger);
+ mProjectCallback.setResourceResolver(mResourceResolver);
+ RenderSecurityManager securityManager = createSecurityManager();
+ try {
+ securityManager.setActive(true, mCredential);
+ synchronized (RENDERING_LOCK) {
+ return mLayoutLib.createSession(params);
+ }
+ } catch (RuntimeException t) {
+ // Exceptions from the bridge
+ mLogger.error(null, t.getLocalizedMessage(), t, null);
+ throw t;
+ } finally {
+ securityManager.dispose(mCredential);
+ mProjectCallback.setLogger(null);
+ mProjectCallback.setResourceResolver(null);
+ }
+ }
+
+ /**
+ * Renders the given resource value (which should refer to a drawable) and returns it
+ * as an image
+ *
+ * @param drawableResourceValue the drawable resource value to be rendered, or null
+ * @return the image, or null if something went wrong
+ */
+ public BufferedImage renderDrawable(ResourceValue drawableResourceValue) {
+ if (drawableResourceValue == null) {
+ return null;
+ }
+
+ finishConfiguration();
+
+ HardwareConfig hardwareConfig = mHardwareConfigHelper.getConfig();
+
+ DrawableParams params = new DrawableParams(drawableResourceValue, mProject, hardwareConfig,
+ mResourceResolver, mProjectCallback, mMinSdkVersion,
+ mTargetSdkVersion, mLogger);
+ params.setAssetRepository(new AssetRepository());
+ params.setForceNoDecor();
+ Result result = mLayoutLib.renderDrawable(params);
+ if (result != null && result.isSuccess()) {
+ Object data = result.getData();
+ if (data instanceof BufferedImage) {
+ return (BufferedImage) data;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Measure the children of the given parent node, applying the given filter to the
+ * pull parser's attribute values.
+ *
+ * @param parent the parent node to measure children for
+ * @param filter the filter to apply to the attribute values
+ * @return a map from node children of the parent to new bounds of the nodes
+ */
+ public Map<INode, Rect> measureChildren(INode parent,
+ final IClientRulesEngine.AttributeFilter filter) {
+ finishConfiguration();
+ HardwareConfig hardwareConfig = mHardwareConfigHelper.getConfig();
+
+ final NodeFactory mNodeFactory = mEditor.getCanvasControl().getNodeFactory();
+ UiElementNode parentNode = ((NodeProxy) parent).getNode();
+ UiElementPullParser topParser = new UiElementPullParser(parentNode,
+ false, Collections.<UiElementNode>emptySet(), hardwareConfig.getDensity(),
+ mProject) {
+ @Override
+ public String getAttributeValue(String namespace, String localName) {
+ if (filter != null) {
+ Object cookie = getViewCookie();
+ if (cookie instanceof UiViewElementNode) {
+ NodeProxy node = mNodeFactory.create((UiViewElementNode) cookie);
+ if (node != null) {
+ String value = filter.getAttribute(node, namespace, localName);
+ if (value != null) {
+ return value;
+ }
+ // null means no preference, not "unset".
+ }
+ }
+ }
+
+ return super.getAttributeValue(namespace, localName);
+ }
+
+ /**
+ * The parser usually assumes that the top level node is a document node that
+ * should be skipped, and that's not the case when we render in the middle of
+ * the tree, so override {@link UiElementPullParser#onNextFromStartDocument}
+ * to change this behavior
+ */
+ @Override
+ public void onNextFromStartDocument() {
+ mParsingState = START_TAG;
+ }
+ };
+
+ SessionParams params = new SessionParams(
+ topParser,
+ RenderingMode.FULL_EXPAND,
+ mProject /* projectKey */,
+ hardwareConfig,
+ mResourceResolver,
+ mProjectCallback,
+ mMinSdkVersion,
+ mTargetSdkVersion,
+ mLogger);
+ params.setLayoutOnly();
+ params.setForceNoDecor();
+ params.setAssetRepository(new AssetRepository());
+
+ RenderSession session = null;
+ mProjectCallback.setLogger(mLogger);
+ mProjectCallback.setResourceResolver(mResourceResolver);
+ RenderSecurityManager securityManager = createSecurityManager();
+ try {
+ securityManager.setActive(true, mCredential);
+ synchronized (RENDERING_LOCK) {
+ session = mLayoutLib.createSession(params);
+ }
+ if (session.getResult().isSuccess()) {
+ assert session.getRootViews().size() == 1;
+ ViewInfo root = session.getRootViews().get(0);
+ List<ViewInfo> children = root.getChildren();
+ Map<INode, Rect> map = new HashMap<INode, Rect>(children.size());
+ for (ViewInfo info : children) {
+ if (info.getCookie() instanceof UiViewElementNode) {
+ UiViewElementNode uiNode = (UiViewElementNode) info.getCookie();
+ NodeProxy node = mNodeFactory.create(uiNode);
+ map.put(node, new Rect(info.getLeft(), info.getTop(),
+ info.getRight() - info.getLeft(),
+ info.getBottom() - info.getTop()));
+ }
+ }
+
+ return map;
+ }
+ } catch (RuntimeException t) {
+ // Exceptions from the bridge
+ mLogger.error(null, t.getLocalizedMessage(), t, null);
+ throw t;
+ } finally {
+ securityManager.dispose(mCredential);
+ mProjectCallback.setLogger(null);
+ mProjectCallback.setResourceResolver(null);
+ if (session != null) {
+ session.dispose();
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java
new file mode 100644
index 000000000..4d51c07de
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ResizeGesture.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2011 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.ide.common.api.DropFeedback;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.ResizePolicy;
+import com.android.ide.common.api.SegmentType;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.Position;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.utils.Pair;
+
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.graphics.GC;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A {@link ResizeGesture} is a gesture for resizing a selected widget. It is initiated
+ * by a drag of a {@link SelectionHandle}.
+ */
+public class ResizeGesture extends Gesture {
+ /** The {@link Overlay} drawn for the gesture feedback. */
+ private ResizeOverlay mOverlay;
+
+ /** The canvas associated with this gesture. */
+ private LayoutCanvas mCanvas;
+
+ /** The selection handle we're dragging to perform this resize */
+ private SelectionHandle mHandle;
+
+ private NodeProxy mParentNode;
+ private NodeProxy mChildNode;
+ private DropFeedback mFeedback;
+ private ResizePolicy mResizePolicy;
+ private SegmentType mHorizontalEdge;
+ private SegmentType mVerticalEdge;
+
+ /**
+ * Creates a new marquee selection (selection swiping).
+ *
+ * @param canvas The canvas where selection is performed.
+ * @param item The selected item the handle corresponds to
+ * @param handle The handle being dragged to perform the resize
+ */
+ public ResizeGesture(LayoutCanvas canvas, SelectionItem item, SelectionHandle handle) {
+ mCanvas = canvas;
+ mHandle = handle;
+
+ mChildNode = item.getNode();
+ mParentNode = (NodeProxy) mChildNode.getParent();
+ mResizePolicy = item.getResizePolicy();
+ mHorizontalEdge = getHorizontalEdgeType(mHandle);
+ mVerticalEdge = getVerticalEdgeType(mHandle);
+ }
+
+ @Override
+ public void begin(ControlPoint pos, int startMask) {
+ super.begin(pos, startMask);
+
+ mCanvas.getSelectionOverlay().setHidden(true);
+
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ Rect newBounds = getNewBounds(pos);
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ CanvasViewInfo childInfo = viewHierarchy.findViewInfoFor(mChildNode);
+ CanvasViewInfo parentInfo = viewHierarchy.findViewInfoFor(mParentNode);
+ Object childView = childInfo != null ? childInfo.getViewObject() : null;
+ Object parentView = parentInfo != null ? parentInfo.getViewObject() : null;
+ mFeedback = rulesEngine.callOnResizeBegin(mChildNode, mParentNode, newBounds,
+ mHorizontalEdge, mVerticalEdge, childView, parentView);
+ update(pos);
+ mCanvas.getGestureManager().updateMessage(mFeedback);
+ }
+
+ @Override
+ public boolean keyPressed(KeyEvent event) {
+ update(mCanvas.getGestureManager().getCurrentControlPoint());
+ mCanvas.redraw();
+ return true;
+ }
+
+ @Override
+ public boolean keyReleased(KeyEvent event) {
+ update(mCanvas.getGestureManager().getCurrentControlPoint());
+ mCanvas.redraw();
+ return true;
+ }
+
+ @Override
+ public void update(ControlPoint pos) {
+ super.update(pos);
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ Rect newBounds = getNewBounds(pos);
+ int modifierMask = mCanvas.getGestureManager().getRuleModifierMask();
+ rulesEngine.callOnResizeUpdate(mFeedback, mChildNode, mParentNode, newBounds,
+ modifierMask);
+ mCanvas.getGestureManager().updateMessage(mFeedback);
+ }
+
+ @Override
+ public void end(ControlPoint pos, boolean canceled) {
+ super.end(pos, canceled);
+
+ if (!canceled) {
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ Rect newBounds = getNewBounds(pos);
+ rulesEngine.callOnResizeEnd(mFeedback, mChildNode, mParentNode, newBounds);
+ }
+
+ mCanvas.getSelectionOverlay().setHidden(false);
+ }
+
+ @Override
+ public Pair<Boolean, Boolean> getTooltipPosition() {
+ return Pair.of(mHorizontalEdge != SegmentType.TOP, mVerticalEdge != SegmentType.LEFT);
+ }
+
+ /**
+ * For the new mouse position, compute the resized bounds (the bounding rectangle that
+ * the view should be resized to). This is not just a width or height, since in some
+ * cases resizing will change the x/y position of the view as well (for example, in
+ * RelativeLayout or in AbsoluteLayout).
+ */
+ private Rect getNewBounds(ControlPoint pos) {
+ LayoutPoint p = pos.toLayout();
+ LayoutPoint start = mStart.toLayout();
+ Rect b = mChildNode.getBounds();
+ Position direction = mHandle.getPosition();
+
+ int x = b.x;
+ int y = b.y;
+ int w = b.w;
+ int h = b.h;
+ int deltaX = p.x - start.x;
+ int deltaY = p.y - start.y;
+
+ if (deltaX == 0 && deltaY == 0) {
+ // No move - just use the existing bounds
+ return b;
+ }
+
+ if (mResizePolicy.isAspectPreserving() && w != 0 && h != 0) {
+ double aspectRatio = w / (double) h;
+ int newW = Math.abs(b.w + (direction.isLeft() ? -deltaX : deltaX));
+ int newH = Math.abs(b.h + (direction.isTop() ? -deltaY : deltaY));
+ double newAspectRatio = newW / (double) newH;
+ if (newH == 0 || newAspectRatio > aspectRatio) {
+ deltaY = (int) (deltaX / aspectRatio);
+ } else {
+ deltaX = (int) (deltaY * aspectRatio);
+ }
+ }
+ if (direction.isLeft()) {
+ // The user is dragging the left edge, so the position is anchored on the
+ // right.
+ int x2 = b.x + b.w;
+ int nx1 = b.x + deltaX;
+ if (nx1 <= x2) {
+ x = nx1;
+ w = x2 - x;
+ } else {
+ w = 0;
+ x = x2;
+ }
+ } else if (direction.isRight()) {
+ // The user is dragging the right edge, so the position is anchored on the
+ // left.
+ int nx2 = b.x + b.w + deltaX;
+ if (nx2 >= b.x) {
+ w = nx2 - b.x;
+ } else {
+ w = 0;
+ }
+ } else {
+ assert direction == Position.BOTTOM_MIDDLE || direction == Position.TOP_MIDDLE;
+ }
+
+ if (direction.isTop()) {
+ // The user is dragging the top edge, so the position is anchored on the
+ // bottom.
+ int y2 = b.y + b.h;
+ int ny1 = b.y + deltaY;
+ if (ny1 < y2) {
+ y = ny1;
+ h = y2 - y;
+ } else {
+ h = 0;
+ y = y2;
+ }
+ } else if (direction.isBottom()) {
+ // The user is dragging the bottom edge, so the position is anchored on the
+ // top.
+ int ny2 = b.y + b.h + deltaY;
+ if (ny2 >= b.y) {
+ h = ny2 - b.y;
+ } else {
+ h = 0;
+ }
+ } else {
+ assert direction == Position.LEFT_MIDDLE || direction == Position.RIGHT_MIDDLE;
+ }
+
+ return new Rect(x, y, w, h);
+ }
+
+ private static SegmentType getHorizontalEdgeType(SelectionHandle handle) {
+ switch (handle.getPosition()) {
+ case BOTTOM_LEFT:
+ case BOTTOM_RIGHT:
+ case BOTTOM_MIDDLE:
+ return SegmentType.BOTTOM;
+ case LEFT_MIDDLE:
+ case RIGHT_MIDDLE:
+ return null;
+ case TOP_LEFT:
+ case TOP_MIDDLE:
+ case TOP_RIGHT:
+ return SegmentType.TOP;
+ default: assert false : handle.getPosition();
+ }
+ return null;
+ }
+
+ private static SegmentType getVerticalEdgeType(SelectionHandle handle) {
+ switch (handle.getPosition()) {
+ case TOP_LEFT:
+ case LEFT_MIDDLE:
+ case BOTTOM_LEFT:
+ return SegmentType.LEFT;
+ case BOTTOM_MIDDLE:
+ case TOP_MIDDLE:
+ return null;
+ case TOP_RIGHT:
+ case RIGHT_MIDDLE:
+ case BOTTOM_RIGHT:
+ return SegmentType.RIGHT;
+ default: assert false : handle.getPosition();
+ }
+ return null;
+ }
+
+
+ @Override
+ public List<Overlay> createOverlays() {
+ mOverlay = new ResizeOverlay();
+ return Collections.<Overlay> singletonList(mOverlay);
+ }
+
+ /**
+ * An {@link Overlay} to paint the resize feedback. This just delegates to the
+ * layout rule for the parent which is handling the resizing.
+ */
+ private class ResizeOverlay extends Overlay {
+ @Override
+ public void paint(GC gc) {
+ if (mChildNode != null && mFeedback != null) {
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ rulesEngine.callDropFeedbackPaint(mCanvas.getGcWrapper(), mChildNode, mFeedback);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandle.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandle.java
new file mode 100644
index 000000000..c2db2431c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandle.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2011 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 org.eclipse.swt.SWT;
+
+/**
+ * A selection handle is a small rectangle on the border of a selected view which lets you
+ * change the size of the view by dragging it.
+ */
+public class SelectionHandle {
+ /**
+ * Size of the selection handle radius, in control coordinates. Note that this isn't
+ * necessarily a <b>circular</b> radius; in the case of a rectangular handle, the
+ * width and the height are both equal to this radius.
+ * Note also that this radius is in <b>control</b> coordinates, whereas the rest
+ * of the class operates in layout coordinates. This is because we do not want the
+ * selection handles to grow or shrink along with the screen zoom; they are always
+ * at the given pixel size in the control.
+ */
+ public final static int PIXEL_RADIUS = 3;
+
+ /**
+ * Extra number of pixels to look beyond the actual radius of the selection handle
+ * when matching mouse positions to handles
+ */
+ public final static int PIXEL_MARGIN = 2;
+
+ /** The position of the handle in the selection rectangle */
+ enum Position {
+ TOP_MIDDLE(SWT.CURSOR_SIZEN),
+ TOP_RIGHT(SWT.CURSOR_SIZENE),
+ RIGHT_MIDDLE(SWT.CURSOR_SIZEE),
+ BOTTOM_RIGHT(SWT.CURSOR_SIZESE),
+ BOTTOM_MIDDLE(SWT.CURSOR_SIZES),
+ BOTTOM_LEFT(SWT.CURSOR_SIZESW),
+ LEFT_MIDDLE(SWT.CURSOR_SIZEW),
+ TOP_LEFT(SWT.CURSOR_SIZENW);
+
+ /** Corresponding SWT cursor value */
+ private int mSwtCursor;
+
+ private Position(int swtCursor) {
+ mSwtCursor = swtCursor;
+ }
+
+ private int getCursorType() {
+ return mSwtCursor;
+ }
+
+ /** Is the {@link SelectionHandle} somewhere on the left edge? */
+ boolean isLeft() {
+ return this == TOP_LEFT || this == LEFT_MIDDLE || this == BOTTOM_LEFT;
+ }
+
+ /** Is the {@link SelectionHandle} somewhere on the right edge? */
+ boolean isRight() {
+ return this == TOP_RIGHT || this == RIGHT_MIDDLE || this == BOTTOM_RIGHT;
+ }
+
+ /** Is the {@link SelectionHandle} somewhere on the top edge? */
+ boolean isTop() {
+ return this == TOP_LEFT || this == TOP_MIDDLE || this == TOP_RIGHT;
+ }
+
+ /** Is the {@link SelectionHandle} somewhere on the bottom edge? */
+ boolean isBottom() {
+ return this == BOTTOM_LEFT || this == BOTTOM_MIDDLE || this == BOTTOM_RIGHT;
+ }
+ };
+
+ /** The x coordinate of the center of the selection handle */
+ public final int centerX;
+ /** The y coordinate of the center of the selection handle */
+ public final int centerY;
+ /** The position of the handle in the selection rectangle */
+ private final Position mPosition;
+
+ /**
+ * Constructs a new {@link SelectionHandle} at the given layout coordinate
+ * corresponding to a handle at the given {@link Position}.
+ *
+ * @param centerX the x coordinate of the center of the selection handle
+ * @param centerY y coordinate of the center of the selection handle
+ * @param position the position of the handle in the selection rectangle
+ */
+ public SelectionHandle(int centerX, int centerY, Position position) {
+ mPosition = position;
+ this.centerX = centerX;
+ this.centerY = centerY;
+ }
+
+ /**
+ * Determines whether the given {@link LayoutPoint} is within the given distance in
+ * layout coordinates. The distance should incorporate at least the equivalent
+ * distance to the control coordinate space {@link #PIXEL_RADIUS}, but usually with a
+ * few extra pixels added in to make the corners easier to target.
+ *
+ * @param point the mouse position in layout coordinates
+ * @param distance the distance from the center of the handle to check whether the
+ * point fits within
+ * @return true if the given point is within the given distance of this handle
+ */
+ public boolean contains(LayoutPoint point, int distance) {
+ return (point.x >= centerX - distance
+ && point.x <= centerX + distance
+ && point.y >= centerY - distance
+ && point.y <= centerY + distance);
+ }
+
+ /**
+ * Returns the position of the handle in the selection rectangle
+ *
+ * @return the position of the handle in the selection rectangle
+ */
+ public Position getPosition() {
+ return mPosition;
+ }
+
+ /**
+ * Returns the SWT cursor type to use for this selection handle
+ *
+ * @return the position of the handle in the selection rectangle
+ */
+ public int getSwtCursorType() {
+ return mPosition.getCursorType();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandles.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandles.java
new file mode 100644
index 000000000..6d7f34a66
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionHandles.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2011 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.ide.common.api.Margins;
+import com.android.ide.common.api.Rect;
+import com.android.ide.common.api.ResizePolicy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.Position;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * The {@link SelectionHandles} of a {@link SelectionItem} are the set of
+ * {@link SelectionHandle} objects (possibly empty, for non-resizable objects) the user
+ * can manipulate to resize a widget.
+ */
+public class SelectionHandles implements Iterable<SelectionHandle> {
+ private final SelectionItem mItem;
+ private List<SelectionHandle> mHandles;
+
+ /**
+ * Constructs a new {@link SelectionHandles} object for the given {link
+ * {@link SelectionItem}
+ * @param item the item to create {@link SelectionHandles} for
+ */
+ public SelectionHandles(SelectionItem item) {
+ mItem = item;
+
+ createHandles(item.getCanvas());
+ }
+
+ /**
+ * Find a specific {@link SelectionHandle} from this set of {@link SelectionHandles},
+ * which is within the given distance (in layout coordinates) from the center of the
+ * {@link SelectionHandle}.
+ *
+ * @param point the mouse position (in layout coordinates) to test
+ * @param distance the maximum distance from the handle center to accept
+ * @return a {@link SelectionHandle} under the point, or null if not found
+ */
+ public SelectionHandle findHandle(LayoutPoint point, int distance) {
+ for (SelectionHandle handle : mHandles) {
+ if (handle.contains(point, distance)) {
+ return handle;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Create the {@link SelectionHandle} objects for the selection item, according to its
+ * {@link ResizePolicy}.
+ */
+ private void createHandles(LayoutCanvas canvas) {
+ NodeProxy selectedNode = mItem.getNode();
+ Rect r = selectedNode.getBounds();
+ if (!r.isValid()) {
+ mHandles = Collections.emptyList();
+ return;
+ }
+
+ ResizePolicy resizability = mItem.getResizePolicy();
+ if (resizability.isResizable()) {
+ mHandles = new ArrayList<SelectionHandle>(8);
+ boolean left = resizability.leftAllowed();
+ boolean right = resizability.rightAllowed();
+ boolean top = resizability.topAllowed();
+ boolean bottom = resizability.bottomAllowed();
+ int x1 = r.x;
+ int y1 = r.y;
+ int w = r.w;
+ int h = r.h;
+ int x2 = x1 + w;
+ int y2 = y1 + h;
+
+ Margins insets = canvas.getInsets(mItem.getNode().getFqcn());
+ if (insets != null) {
+ x1 += insets.left;
+ x2 -= insets.right;
+ y1 += insets.top;
+ y2 -= insets.bottom;
+ }
+
+ int mx = (x1 + x2) / 2;
+ int my = (y1 + y2) / 2;
+
+ if (left) {
+ mHandles.add(new SelectionHandle(x1, my, Position.LEFT_MIDDLE));
+ if (top) {
+ mHandles.add(new SelectionHandle(x1, y1, Position.TOP_LEFT));
+ }
+ if (bottom) {
+ mHandles.add(new SelectionHandle(x1, y2, Position.BOTTOM_LEFT));
+ }
+ }
+ if (right) {
+ mHandles.add(new SelectionHandle(x2, my, Position.RIGHT_MIDDLE));
+ if (top) {
+ mHandles.add(new SelectionHandle(x2, y1, Position.TOP_RIGHT));
+ }
+ if (bottom) {
+ mHandles.add(new SelectionHandle(x2, y2, Position.BOTTOM_RIGHT));
+ }
+ }
+ if (top) {
+ mHandles.add(new SelectionHandle(mx, y1, Position.TOP_MIDDLE));
+ }
+ if (bottom) {
+ mHandles.add(new SelectionHandle(mx, y2, Position.BOTTOM_MIDDLE));
+ }
+ } else {
+ mHandles = Collections.emptyList();
+ }
+ }
+
+ // Implements Iterable<SelectionHandle>
+ @Override
+ public Iterator<SelectionHandle> iterator() {
+ return mHandles.iterator();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java
new file mode 100644
index 000000000..d104e379e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java
@@ -0,0 +1,252 @@
+/*
+ * 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.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.ResizePolicy;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+
+import org.eclipse.swt.graphics.Rectangle;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents one selection in {@link LayoutCanvas}.
+ */
+class SelectionItem {
+
+ /** The associated {@link LayoutCanvas} */
+ private LayoutCanvas mCanvas;
+
+ /** Current selected view info. Can be null. */
+ private final CanvasViewInfo mCanvasViewInfo;
+
+ /** Current selection border rectangle. Null when mCanvasViewInfo is null . */
+ private final Rectangle mRect;
+
+ /** The node proxy for drawing the selection. Null when mCanvasViewInfo is null. */
+ private final NodeProxy mNodeProxy;
+
+ /** The resize policy for this selection item */
+ private ResizePolicy mResizePolicy;
+
+ /** The selection handles for this item */
+ private SelectionHandles mHandles;
+
+ /**
+ * Creates a new {@link SelectionItem} object.
+ * @param canvas the associated canvas
+ * @param canvasViewInfo The view info being selected. Must not be null.
+ */
+ public SelectionItem(LayoutCanvas canvas, CanvasViewInfo canvasViewInfo) {
+ assert canvasViewInfo != null;
+
+ mCanvas = canvas;
+ mCanvasViewInfo = canvasViewInfo;
+
+ if (canvasViewInfo == null) {
+ mRect = null;
+ mNodeProxy = null;
+ } else {
+ Rectangle r = canvasViewInfo.getSelectionRect();
+ mRect = new Rectangle(r.x, r.y, r.width, r.height);
+ mNodeProxy = mCanvas.getNodeFactory().create(canvasViewInfo);
+ }
+ }
+
+ /**
+ * Returns true when this selection item represents the root, the top level
+ * layout element in the editor.
+ *
+ * @return True if and only if this element is at the root of the hierarchy
+ */
+ public boolean isRoot() {
+ return mCanvasViewInfo.isRoot();
+ }
+
+ /**
+ * Returns true if this item represents a widget that should not be manipulated by the
+ * user.
+ *
+ * @return True if this widget should not be manipulated directly by the user
+ */
+ public boolean isHidden() {
+ return mCanvasViewInfo.isHidden();
+ }
+
+ /**
+ * Returns the selected view info. Cannot be null.
+ *
+ * @return the selected view info. Cannot be null.
+ */
+ @NonNull
+ public CanvasViewInfo getViewInfo() {
+ return mCanvasViewInfo;
+ }
+
+ /**
+ * Returns the selected node.
+ *
+ * @return the selected node, or null
+ */
+ @Nullable
+ public UiViewElementNode getUiNode() {
+ return mCanvasViewInfo.getUiViewNode();
+ }
+
+ /**
+ * Returns the selection border rectangle. Cannot be null.
+ *
+ * @return the selection border rectangle, never null
+ */
+ public Rectangle getRect() {
+ return mRect;
+ }
+
+ /** Returns the node associated with this selection (may be null) */
+ @Nullable
+ NodeProxy getNode() {
+ return mNodeProxy;
+ }
+
+ /** Returns the canvas associated with this selection (never null) */
+ @NonNull
+ LayoutCanvas getCanvas() {
+ return mCanvas;
+ }
+
+ //----
+
+ /**
+ * Gets the XML text from the given selection for a text transfer.
+ * The returned string can be empty but not null.
+ */
+ @NonNull
+ static String getAsText(LayoutCanvas canvas, List<SelectionItem> selection) {
+ StringBuilder sb = new StringBuilder();
+
+ LayoutEditorDelegate layoutEditorDelegate = canvas.getEditorDelegate();
+ for (SelectionItem cs : selection) {
+ CanvasViewInfo vi = cs.getViewInfo();
+ UiViewElementNode key = vi.getUiViewNode();
+ Node node = key.getXmlNode();
+ String t = layoutEditorDelegate.getEditor().getXmlText(node);
+ if (t != null) {
+ if (sb.length() > 0) {
+ sb.append('\n');
+ }
+ sb.append(t);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Returns elements representing the given selection of canvas items.
+ *
+ * @param items Items to wrap in elements
+ * @return An array of wrapper elements. Never null.
+ */
+ @NonNull
+ static SimpleElement[] getAsElements(@NonNull List<SelectionItem> items) {
+ return getAsElements(items, null);
+ }
+
+ /**
+ * Returns elements representing the given selection of canvas items.
+ *
+ * @param items Items to wrap in elements
+ * @param primary The primary selected item which should be listed first
+ * @return An array of wrapper elements. Never null.
+ */
+ @NonNull
+ static SimpleElement[] getAsElements(
+ @NonNull List<SelectionItem> items,
+ @Nullable SelectionItem primary) {
+ List<SimpleElement> elements = new ArrayList<SimpleElement>();
+
+ if (primary != null) {
+ CanvasViewInfo vi = primary.getViewInfo();
+ SimpleElement e = vi.toSimpleElement();
+ e.setSelectionItem(primary);
+ elements.add(e);
+ }
+
+ for (SelectionItem cs : items) {
+ if (cs == primary) {
+ // Already handled
+ continue;
+ }
+
+ CanvasViewInfo vi = cs.getViewInfo();
+ SimpleElement e = vi.toSimpleElement();
+ e.setSelectionItem(cs);
+ elements.add(e);
+ }
+
+ return elements.toArray(new SimpleElement[elements.size()]);
+ }
+
+ /**
+ * Returns true if this selection item is a layout
+ *
+ * @return true if this selection item is a layout
+ */
+ public boolean isLayout() {
+ UiViewElementNode node = mCanvasViewInfo.getUiViewNode();
+ if (node != null) {
+ return node.getDescriptor().hasChildren();
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the {@link SelectionHandles} for this {@link SelectionItem}. Never null.
+ *
+ * @return the {@link SelectionHandles} for this {@link SelectionItem}, never null
+ */
+ @NonNull
+ public SelectionHandles getSelectionHandles() {
+ if (mHandles == null) {
+ mHandles = new SelectionHandles(this);
+ }
+
+ return mHandles;
+ }
+
+ /**
+ * Returns the {@link ResizePolicy} for this item
+ *
+ * @return the {@link ResizePolicy} for this item, never null
+ */
+ @NonNull
+ public ResizePolicy getResizePolicy() {
+ if (mResizePolicy == null && mNodeProxy != null) {
+ mResizePolicy = ViewMetadataRepository.get().getResizePolicy(mNodeProxy.getFqcn());
+ }
+
+ return mResizePolicy;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java
new file mode 100644
index 000000000..eb3d6f290
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java
@@ -0,0 +1,1262 @@
+/*
+ * Copyright (C) 2010 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 static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.FQCN_SPACE;
+import static com.android.SdkConstants.FQCN_SPACE_V7;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_MARGIN;
+import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_RADIUS;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.layout.BaseViewRule;
+import com.android.ide.common.layout.GridLayoutRule;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard;
+import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult;
+import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
+import com.android.resources.ResourceType;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.ListenerList;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.dialogs.InputDialog;
+import org.eclipse.jface.util.SafeRunnable;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MenuDetectEvent;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.ui.IWorkbenchPartSite;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+/**
+ * The {@link SelectionManager} manages the selection in the canvas editor.
+ * It holds (and can be asked about) the set of selected items, and it also has
+ * operations for manipulating the selection - such as toggling items, copying
+ * the selection to the clipboard, etc.
+ * <p/>
+ * This class implements {@link ISelectionProvider} so that it can delegate
+ * the selection provider from the {@link LayoutCanvasViewer}.
+ * <p/>
+ * Note that {@link LayoutCanvasViewer} sets a selection change listener on this
+ * manager so that it can invoke its own fireSelectionChanged when the canvas'
+ * selection changes.
+ */
+public class SelectionManager implements ISelectionProvider {
+
+ private LayoutCanvas mCanvas;
+
+ /** The current selection list. The list is never null, however it can be empty. */
+ private final LinkedList<SelectionItem> mSelections = new LinkedList<SelectionItem>();
+
+ /** An unmodifiable view of {@link #mSelections}. */
+ private final List<SelectionItem> mUnmodifiableSelection =
+ Collections.unmodifiableList(mSelections);
+
+ /** Barrier set when updating the selection to prevent from recursively
+ * invoking ourselves. */
+ private boolean mInsideUpdateSelection;
+
+ /**
+ * The <em>current</em> alternate selection, if any, which changes when the Alt key is
+ * used during a selection. Can be null.
+ */
+ private CanvasAlternateSelection mAltSelection;
+
+ /** List of clients listening to selection changes. */
+ private final ListenerList mSelectionListeners = new ListenerList();
+
+ /**
+ * Constructs a new {@link SelectionManager} associated with the given layout canvas.
+ *
+ * @param layoutCanvas The layout canvas to create a {@link SelectionManager} for.
+ */
+ public SelectionManager(LayoutCanvas layoutCanvas) {
+ mCanvas = layoutCanvas;
+ }
+
+ @Override
+ public void addSelectionChangedListener(ISelectionChangedListener listener) {
+ mSelectionListeners.add(listener);
+ }
+
+ @Override
+ public void removeSelectionChangedListener(ISelectionChangedListener listener) {
+ mSelectionListeners.remove(listener);
+ }
+
+ /**
+ * Returns the native {@link SelectionItem} list.
+ *
+ * @return An immutable list of {@link SelectionItem}. Can be empty but not null.
+ */
+ @NonNull
+ List<SelectionItem> getSelections() {
+ return mUnmodifiableSelection;
+ }
+
+ /**
+ * Return a snapshot/copy of the selection. Useful for clipboards etc where we
+ * don't want the returned copy to be affected by future edits to the selection.
+ *
+ * @return A copy of the current selection. Never null.
+ */
+ @NonNull
+ public List<SelectionItem> getSnapshot() {
+ if (mSelectionListeners.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ return new ArrayList<SelectionItem>(mSelections);
+ }
+
+ /**
+ * Returns a {@link TreeSelection} where each {@link TreePath} item is
+ * actually a {@link CanvasViewInfo}.
+ */
+ @Override
+ public ISelection getSelection() {
+ if (mSelections.isEmpty()) {
+ return TreeSelection.EMPTY;
+ }
+
+ ArrayList<TreePath> paths = new ArrayList<TreePath>();
+
+ for (SelectionItem cs : mSelections) {
+ CanvasViewInfo vi = cs.getViewInfo();
+ if (vi != null) {
+ paths.add(getTreePath(vi));
+ }
+ }
+
+ return new TreeSelection(paths.toArray(new TreePath[paths.size()]));
+ }
+
+ /**
+ * Create a {@link TreePath} from the given view info
+ *
+ * @param viewInfo the view info to look up a tree path for
+ * @return a {@link TreePath} for the given view info
+ */
+ public static TreePath getTreePath(CanvasViewInfo viewInfo) {
+ ArrayList<Object> segments = new ArrayList<Object>();
+ while (viewInfo != null) {
+ segments.add(0, viewInfo);
+ viewInfo = viewInfo.getParent();
+ }
+
+ return new TreePath(segments.toArray());
+ }
+
+ /**
+ * Sets the selection. It must be an {@link ITreeSelection} where each segment
+ * of the tree path is a {@link CanvasViewInfo}. A null selection is considered
+ * as an empty selection.
+ * <p/>
+ * This method is invoked by {@link LayoutCanvasViewer#setSelection(ISelection)}
+ * in response to an <em>outside</em> selection (compatible with ours) that has
+ * changed. Typically it means the outline selection has changed and we're
+ * synchronizing ours to match.
+ */
+ @Override
+ public void setSelection(ISelection selection) {
+ if (mInsideUpdateSelection) {
+ return;
+ }
+
+ boolean changed = false;
+ try {
+ mInsideUpdateSelection = true;
+
+ if (selection == null) {
+ selection = TreeSelection.EMPTY;
+ }
+
+ if (selection instanceof ITreeSelection) {
+ ITreeSelection treeSel = (ITreeSelection) selection;
+
+ if (treeSel.isEmpty()) {
+ // Clear existing selection, if any
+ if (!mSelections.isEmpty()) {
+ mSelections.clear();
+ mAltSelection = null;
+ updateActionsFromSelection();
+ redraw();
+ }
+ return;
+ }
+
+ boolean redoLayout = false;
+
+ // Create a list of all currently selected view infos
+ Set<CanvasViewInfo> oldSelected = new HashSet<CanvasViewInfo>();
+ for (SelectionItem cs : mSelections) {
+ oldSelected.add(cs.getViewInfo());
+ }
+
+ // Go thru new selection and take care of selecting new items
+ // or marking those which are the same as in the current selection
+ for (TreePath path : treeSel.getPaths()) {
+ Object seg = path.getLastSegment();
+ if (seg instanceof CanvasViewInfo) {
+ CanvasViewInfo newVi = (CanvasViewInfo) seg;
+ if (oldSelected.contains(newVi)) {
+ // This view info is already selected. Remove it from the
+ // oldSelected list so that we don't deselect it later.
+ oldSelected.remove(newVi);
+ } else {
+ // This view info is not already selected. Select it now.
+
+ // reset alternate selection if any
+ mAltSelection = null;
+ // otherwise add it.
+ mSelections.add(createSelection(newVi));
+ changed = true;
+ }
+ if (newVi.isInvisible()) {
+ redoLayout = true;
+ }
+ } else {
+ // Unrelated selection (e.g. user clicked in the Project Explorer
+ // or something) -- just ignore these
+ return;
+ }
+ }
+
+ // Deselect old selected items that are not in the new one
+ for (CanvasViewInfo vi : oldSelected) {
+ if (vi.isExploded()) {
+ redoLayout = true;
+ }
+ deselect(vi);
+ changed = true;
+ }
+
+ if (redoLayout) {
+ mCanvas.getEditorDelegate().recomputeLayout();
+ }
+ }
+ } finally {
+ mInsideUpdateSelection = false;
+ }
+
+ if (changed) {
+ redraw();
+ fireSelectionChanged();
+ updateActionsFromSelection();
+ }
+ }
+
+ /**
+ * The menu has been activated; ensure that the menu click is over the existing
+ * selection, and if not, update the selection.
+ *
+ * @param e the {@link MenuDetectEvent} which triggered the menu
+ */
+ public void menuClick(MenuDetectEvent e) {
+ LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout();
+
+ // Right click button is used to display a context menu.
+ // If there's an existing selection and the click is anywhere in this selection
+ // and there are no modifiers being used, we don't want to change the selection.
+ // Otherwise we select the item under the cursor.
+
+ for (SelectionItem cs : mSelections) {
+ if (cs.isRoot()) {
+ continue;
+ }
+ if (cs.getRect().contains(p.x, p.y)) {
+ // The cursor is inside the selection. Don't change anything.
+ return;
+ }
+ }
+
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+ selectSingle(vi);
+ }
+
+ /**
+ * Performs selection for a mouse event.
+ * <p/>
+ * Shift key (or Command on the Mac) is used to toggle in multi-selection.
+ * Alt key is used to cycle selection through objects at the same level than
+ * the one pointed at (i.e. click on an object then alt-click to cycle).
+ *
+ * @param e The mouse event which triggered the selection. Cannot be null.
+ * The modifier key mask will be used to determine whether this
+ * is a plain select or a toggle, etc.
+ */
+ public void select(MouseEvent e) {
+ boolean isMultiClick = (e.stateMask & SWT.SHIFT) != 0 ||
+ // On Mac, the Command key is the normal toggle accelerator
+ ((SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) &&
+ (e.stateMask & SWT.COMMAND) != 0);
+ boolean isCycleClick = (e.stateMask & SWT.ALT) != 0;
+
+ LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout();
+
+ if (e.button == 3) {
+ // Right click button is used to display a context menu.
+ // If there's an existing selection and the click is anywhere in this selection
+ // and there are no modifiers being used, we don't want to change the selection.
+ // Otherwise we select the item under the cursor.
+
+ if (!isCycleClick && !isMultiClick) {
+ for (SelectionItem cs : mSelections) {
+ if (cs.getRect().contains(p.x, p.y)) {
+ // The cursor is inside the selection. Don't change anything.
+ return;
+ }
+ }
+ }
+
+ } else if (e.button != 1) {
+ // Click was done with something else than the left button for normal selection
+ // or the right button for context menu.
+ // We don't use mouse button 2 yet (middle mouse, or scroll wheel?) for
+ // anything, so let's not change the selection.
+ return;
+ }
+
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+
+ if (vi != null && vi.isHidden()) {
+ vi = vi.getParent();
+ }
+
+ if (isMultiClick && !isCycleClick) {
+ // Case where shift is pressed: pointed object is toggled.
+
+ // reset alternate selection if any
+ mAltSelection = null;
+
+ // If nothing has been found at the cursor, assume it might be a user error
+ // and avoid clearing the existing selection.
+
+ if (vi != null) {
+ // toggle this selection on-off: remove it if already selected
+ if (deselect(vi)) {
+ if (vi.isExploded()) {
+ mCanvas.getEditorDelegate().recomputeLayout();
+ }
+
+ redraw();
+ return;
+ }
+
+ // otherwise add it.
+ mSelections.add(createSelection(vi));
+ fireSelectionChanged();
+ redraw();
+ }
+
+ } else if (isCycleClick) {
+ // Case where alt is pressed: select or cycle the object pointed at.
+
+ // Note: if shift and alt are pressed, shift is ignored. The alternate selection
+ // mechanism does not reset the current multiple selection unless they intersect.
+
+ // We need to remember the "origin" of the alternate selection, to be
+ // able to continue cycling through it later. If there's no alternate selection,
+ // create one. If there's one but not for the same origin object, create a new
+ // one too.
+ if (mAltSelection == null || mAltSelection.getOriginatingView() != vi) {
+ mAltSelection = new CanvasAlternateSelection(
+ vi, mCanvas.getViewHierarchy().findAltViewInfoAt(p));
+
+ // deselect them all, in case they were partially selected
+ deselectAll(mAltSelection.getAltViews());
+
+ // select the current one
+ CanvasViewInfo vi2 = mAltSelection.getCurrent();
+ if (vi2 != null) {
+ mSelections.addFirst(createSelection(vi2));
+ fireSelectionChanged();
+ }
+ } else {
+ // We're trying to cycle through the current alternate selection.
+ // First remove the current object.
+ CanvasViewInfo vi2 = mAltSelection.getCurrent();
+ deselect(vi2);
+
+ // Now select the next one.
+ vi2 = mAltSelection.getNext();
+ if (vi2 != null) {
+ mSelections.addFirst(createSelection(vi2));
+ fireSelectionChanged();
+ }
+ }
+ redraw();
+
+ } else {
+ // Case where no modifier is pressed: either select or reset the selection.
+ selectSingle(vi);
+ }
+ }
+
+ /**
+ * Removes all the currently selected item and only select the given item.
+ * Issues a redraw() if the selection changes.
+ *
+ * @param vi The new selected item if non-null. Selection becomes empty if null.
+ * @return the item selected, or null if the selection was cleared (e.g. vi was null)
+ */
+ @Nullable
+ SelectionItem selectSingle(CanvasViewInfo vi) {
+ SelectionItem item = null;
+
+ // reset alternate selection if any
+ mAltSelection = null;
+
+ if (vi == null) {
+ // The user clicked outside the bounds of the root element; in that case, just
+ // select the root element.
+ vi = mCanvas.getViewHierarchy().getRoot();
+ }
+
+ boolean redoLayout = hasExplodedItems();
+
+ // reset (multi)selection if any
+ if (!mSelections.isEmpty()) {
+ if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) {
+ // CanvasSelection remains the same, don't touch it.
+ return mSelections.getFirst();
+ }
+ mSelections.clear();
+ }
+
+ if (vi != null) {
+ item = createSelection(vi);
+ mSelections.add(item);
+ if (vi.isInvisible()) {
+ redoLayout = true;
+ }
+ }
+ fireSelectionChanged();
+
+ if (redoLayout) {
+ mCanvas.getEditorDelegate().recomputeLayout();
+ }
+
+ redraw();
+
+ return item;
+ }
+
+ /** Returns true if the view hierarchy is showing exploded items. */
+ private boolean hasExplodedItems() {
+ for (SelectionItem item : mSelections) {
+ if (item.getViewInfo().isExploded()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Selects the given set of {@link CanvasViewInfo}s. This is similar to
+ * {@link #selectSingle} but allows you to make a multi-selection. Issues a
+ * {@link #redraw()}.
+ *
+ * @param viewInfos A collection of {@link CanvasViewInfo} objects to be
+ * selected, or null or empty to clear the selection.
+ */
+ /* package */ void selectMultiple(Collection<CanvasViewInfo> viewInfos) {
+ // reset alternate selection if any
+ mAltSelection = null;
+
+ boolean redoLayout = hasExplodedItems();
+
+ mSelections.clear();
+ if (viewInfos != null) {
+ for (CanvasViewInfo viewInfo : viewInfos) {
+ mSelections.add(createSelection(viewInfo));
+ if (viewInfo.isInvisible()) {
+ redoLayout = true;
+ }
+ }
+ }
+
+ fireSelectionChanged();
+
+ if (redoLayout) {
+ mCanvas.getEditorDelegate().recomputeLayout();
+ }
+
+ redraw();
+ }
+
+ public void select(Collection<INode> nodes) {
+ List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>(nodes.size());
+ for (INode node : nodes) {
+ CanvasViewInfo info = mCanvas.getViewHierarchy().findViewInfoFor(node);
+ if (info != null) {
+ infos.add(info);
+ }
+ }
+ selectMultiple(infos);
+ }
+
+ /**
+ * Selects the visual element corresponding to the given XML node
+ * @param xmlNode The Node whose element we want to select.
+ */
+ /* package */ void select(Node xmlNode) {
+ if (xmlNode == null) {
+ return;
+ } else if (xmlNode.getNodeType() == Node.TEXT_NODE) {
+ xmlNode = xmlNode.getParentNode();
+ }
+
+ CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoFor(xmlNode);
+ if (vi != null && !vi.isRoot()) {
+ selectSingle(vi);
+ }
+ }
+
+ /**
+ * Selects any views that overlap the given selection rectangle.
+ *
+ * @param topLeft The top left corner defining the selection rectangle.
+ * @param bottomRight The bottom right corner defining the selection
+ * rectangle.
+ * @param toggled A set of {@link CanvasViewInfo}s that should be toggled
+ * rather than just added.
+ */
+ public void selectWithin(LayoutPoint topLeft, LayoutPoint bottomRight,
+ Collection<CanvasViewInfo> toggled) {
+ // reset alternate selection if any
+ mAltSelection = null;
+
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ Collection<CanvasViewInfo> viewInfos = viewHierarchy.findWithin(topLeft, bottomRight);
+
+ if (toggled.size() > 0) {
+ // Copy; we're not allowed to touch the passed in collection
+ Set<CanvasViewInfo> result = new HashSet<CanvasViewInfo>(toggled);
+ for (CanvasViewInfo viewInfo : viewInfos) {
+ if (toggled.contains(viewInfo)) {
+ result.remove(viewInfo);
+ } else {
+ result.add(viewInfo);
+ }
+ }
+ viewInfos = result;
+ }
+
+ mSelections.clear();
+ for (CanvasViewInfo viewInfo : viewInfos) {
+ if (viewInfo.isHidden()) {
+ continue;
+ }
+ mSelections.add(createSelection(viewInfo));
+ }
+
+ fireSelectionChanged();
+ redraw();
+ }
+
+ /**
+ * Clears the selection and then selects everything (all views and all their
+ * children).
+ */
+ public void selectAll() {
+ // First clear the current selection, if any.
+ mSelections.clear();
+ mAltSelection = null;
+
+ // Now select everything if there's a valid layout
+ for (CanvasViewInfo vi : mCanvas.getViewHierarchy().findAllViewInfos(false)) {
+ mSelections.add(createSelection(vi));
+ }
+
+ fireSelectionChanged();
+ redraw();
+ }
+
+ /** Clears the selection */
+ public void selectNone() {
+ mSelections.clear();
+ mAltSelection = null;
+ fireSelectionChanged();
+ redraw();
+ }
+
+ /** Selects the parent of the current selection */
+ public void selectParent() {
+ if (mSelections.size() == 1) {
+ CanvasViewInfo parent = mSelections.get(0).getViewInfo().getParent();
+ if (parent != null) {
+ selectSingle(parent);
+ }
+ }
+ }
+
+ /** Finds all widgets in the layout that have the same type as the primary */
+ public void selectSameType() {
+ // Find all
+ if (mSelections.size() == 1) {
+ CanvasViewInfo viewInfo = mSelections.get(0).getViewInfo();
+ ElementDescriptor descriptor = viewInfo.getUiViewNode().getDescriptor();
+ mSelections.clear();
+ mAltSelection = null;
+ addSameType(mCanvas.getViewHierarchy().getRoot(), descriptor);
+ fireSelectionChanged();
+ redraw();
+ }
+ }
+
+ /** Helper for {@link #selectSameType} */
+ private void addSameType(CanvasViewInfo root, ElementDescriptor descriptor) {
+ if (root.getUiViewNode().getDescriptor() == descriptor) {
+ mSelections.add(createSelection(root));
+ }
+
+ for (CanvasViewInfo child : root.getChildren()) {
+ addSameType(child, descriptor);
+ }
+ }
+
+ /** Selects the siblings of the primary */
+ public void selectSiblings() {
+ // Find all
+ if (mSelections.size() == 1) {
+ CanvasViewInfo vi = mSelections.get(0).getViewInfo();
+ mSelections.clear();
+ mAltSelection = null;
+ CanvasViewInfo parent = vi.getParent();
+ if (parent == null) {
+ selectNone();
+ } else {
+ for (CanvasViewInfo child : parent.getChildren()) {
+ mSelections.add(createSelection(child));
+ }
+ fireSelectionChanged();
+ redraw();
+ }
+ }
+ }
+
+ /**
+ * Returns true if and only if there is currently more than one selected
+ * item.
+ *
+ * @return True if more than one item is selected
+ */
+ public boolean hasMultiSelection() {
+ return mSelections.size() > 1;
+ }
+
+ /**
+ * Deselects a view info. Returns true if the object was actually selected.
+ * Callers are responsible for calling redraw() and updateOulineSelection()
+ * after.
+ * @param canvasViewInfo The item to deselect.
+ * @return True if the object was successfully removed from the selection.
+ */
+ public boolean deselect(CanvasViewInfo canvasViewInfo) {
+ if (canvasViewInfo == null) {
+ return false;
+ }
+
+ for (ListIterator<SelectionItem> it = mSelections.listIterator(); it.hasNext(); ) {
+ SelectionItem s = it.next();
+ if (canvasViewInfo == s.getViewInfo()) {
+ it.remove();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Deselects multiple view infos.
+ * Callers are responsible for calling redraw() and updateOulineSelection() after.
+ */
+ private void deselectAll(List<CanvasViewInfo> canvasViewInfos) {
+ for (ListIterator<SelectionItem> it = mSelections.listIterator(); it.hasNext(); ) {
+ SelectionItem s = it.next();
+ if (canvasViewInfos.contains(s.getViewInfo())) {
+ it.remove();
+ }
+ }
+ }
+
+ /** Sync the selection with an updated view info tree */
+ void sync() {
+ // Check if the selection is still the same (based on the object keys)
+ // and eventually recompute their bounds.
+ for (ListIterator<SelectionItem> it = mSelections.listIterator(); it.hasNext(); ) {
+ SelectionItem s = it.next();
+
+ // Check if the selected object still exists
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ UiViewElementNode key = s.getViewInfo().getUiViewNode();
+ CanvasViewInfo vi = viewHierarchy.findViewInfoFor(key);
+
+ // Remove the previous selection -- if the selected object still exists
+ // we need to recompute its bounds in case it moved so we'll insert a new one
+ // at the same place.
+ it.remove();
+ if (vi == null) {
+ vi = findCorresponding(s.getViewInfo(), viewHierarchy.getRoot());
+ }
+ if (vi != null) {
+ it.add(createSelection(vi));
+ }
+ }
+ fireSelectionChanged();
+
+ // remove the current alternate selection views
+ mAltSelection = null;
+ }
+
+ /** Finds the corresponding {@link CanvasViewInfo} in the new hierarchy */
+ private CanvasViewInfo findCorresponding(CanvasViewInfo old, CanvasViewInfo newRoot) {
+ CanvasViewInfo oldParent = old.getParent();
+ if (oldParent != null) {
+ CanvasViewInfo newParent = findCorresponding(oldParent, newRoot);
+ if (newParent == null) {
+ return null;
+ }
+
+ List<CanvasViewInfo> oldSiblings = oldParent.getChildren();
+ List<CanvasViewInfo> newSiblings = newParent.getChildren();
+ Iterator<CanvasViewInfo> oldIterator = oldSiblings.iterator();
+ Iterator<CanvasViewInfo> newIterator = newSiblings.iterator();
+ while (oldIterator.hasNext() && newIterator.hasNext()) {
+ CanvasViewInfo oldSibling = oldIterator.next();
+ CanvasViewInfo newSibling = newIterator.next();
+
+ if (oldSibling.getName().equals(newSibling.getName())) {
+ // Structure has changed: can't do a proper search
+ return null;
+ }
+
+ if (oldSibling == old) {
+ return newSibling;
+ }
+ }
+ } else {
+ return newRoot;
+ }
+
+ return null;
+ }
+
+ /**
+ * Notifies listeners that the selection has changed.
+ */
+ private void fireSelectionChanged() {
+ if (mInsideUpdateSelection) {
+ return;
+ }
+ try {
+ mInsideUpdateSelection = true;
+
+ final SelectionChangedEvent event = new SelectionChangedEvent(this, getSelection());
+
+ SafeRunnable.run(new SafeRunnable() {
+ @Override
+ public void run() {
+ for (Object listener : mSelectionListeners.getListeners()) {
+ ((ISelectionChangedListener) listener).selectionChanged(event);
+ }
+ }
+ });
+
+ updateActionsFromSelection();
+ } finally {
+ mInsideUpdateSelection = false;
+ }
+ }
+
+ /**
+ * Updates menu actions and the layout action bar after a selection change - these are
+ * actions that depend on the selection
+ */
+ private void updateActionsFromSelection() {
+ LayoutEditorDelegate editor = mCanvas.getEditorDelegate();
+ if (editor != null) {
+ // Update menu actions that depend on the selection
+ mCanvas.updateMenuActionState();
+
+ // Update the layout actions bar
+ LayoutActionBar layoutActionBar = editor.getGraphicalEditor().getLayoutActionBar();
+ layoutActionBar.updateSelection();
+ }
+ }
+
+ /**
+ * Sanitizes the selection for a copy/cut or drag operation.
+ * <p/>
+ * Sanitizes the list to make sure all elements have a valid XML attached to it,
+ * that is remove element that have no XML to avoid having to make repeated such
+ * checks in various places after.
+ * <p/>
+ * In case of multiple selection, we also need to remove all children when their
+ * parent is already selected since parents will always be added with all their
+ * children.
+ * <p/>
+ *
+ * @param selection The selection list to be sanitized <b>in-place</b>.
+ * The <code>selection</code> argument should not be {@link #mSelections} -- the
+ * given list is going to be altered and we should never alter the user-made selection.
+ * Instead the caller should provide its own copy.
+ */
+ /* package */ static void sanitize(List<SelectionItem> selection) {
+ if (selection.isEmpty()) {
+ return;
+ }
+
+ for (Iterator<SelectionItem> it = selection.iterator(); it.hasNext(); ) {
+ SelectionItem cs = it.next();
+ CanvasViewInfo vi = cs.getViewInfo();
+ UiViewElementNode key = vi == null ? null : vi.getUiViewNode();
+ Node node = key == null ? null : key.getXmlNode();
+ if (node == null) {
+ // Missing ViewInfo or view key or XML, discard this.
+ it.remove();
+ continue;
+ }
+
+ if (vi != null) {
+ for (Iterator<SelectionItem> it2 = selection.iterator();
+ it2.hasNext(); ) {
+ SelectionItem cs2 = it2.next();
+ if (cs != cs2) {
+ CanvasViewInfo vi2 = cs2.getViewInfo();
+ if (vi.isParent(vi2)) {
+ // vi2 is a parent for vi. Remove vi.
+ it.remove();
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Selects the given list of nodes in the canvas, and returns true iff the
+ * attempt to select was successful.
+ *
+ * @param nodes The collection of nodes to be selected
+ * @param indices A list of indices within the parent for each node, or null
+ * @return True if and only if all nodes were successfully selected
+ */
+ public boolean selectDropped(List<INode> nodes, List<Integer> indices) {
+ assert indices == null || nodes.size() == indices.size();
+
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+
+ // Look up a list of view infos which correspond to the nodes.
+ final Collection<CanvasViewInfo> newChildren = new ArrayList<CanvasViewInfo>();
+ for (int i = 0, n = nodes.size(); i < n; i++) {
+ INode node = nodes.get(i);
+
+ CanvasViewInfo viewInfo = viewHierarchy.findViewInfoFor(node);
+
+ // There are two scenarios where looking up a view info fails.
+ // The first one is that the node was just added and the render has not yet
+ // happened, so the ViewHierarchy has no record of the node. In this case
+ // there is nothing we can do, and the method will return false (which the
+ // caller will use to schedule a second attempt later).
+ // The second scenario is where the nodes *change identity*. This isn't
+ // common, but when a drop handler makes a lot of changes to its children,
+ // for example when dropping into a GridLayout where attributes are adjusted
+ // on nearly all the other children to update row or column attributes
+ // etc, then in some cases Eclipse's DOM model changes the identities of
+ // the nodes when applying all the edits, so the new Node we created (as
+ // well as possibly other nodes) are no longer the children we observe
+ // after the edit, and there are new copies there instead. In this case
+ // the UiViewModel also fails to map the nodes. To work around this,
+ // we track the *indices* (within the parent) during a drop, such that we
+ // know which children (according to their positions) the given nodes
+ // are supposed to map to, and then we use these view infos instead.
+ if (viewInfo == null && node instanceof NodeProxy && indices != null) {
+ INode parent = node.getParent();
+ CanvasViewInfo parentViewInfo = viewHierarchy.findViewInfoFor(parent);
+ if (parentViewInfo != null) {
+ UiViewElementNode parentUiNode = parentViewInfo.getUiViewNode();
+ if (parentUiNode != null) {
+ List<UiElementNode> children = parentUiNode.getUiChildren();
+ int index = indices.get(i);
+ if (index >= 0 && index < children.size()) {
+ UiElementNode replacedNode = children.get(index);
+ viewInfo = viewHierarchy.findViewInfoFor(replacedNode);
+ }
+ }
+ }
+ }
+
+ if (viewInfo != null) {
+ if (nodes.size() > 1 && viewInfo.isHidden()) {
+ // Skip spacers - unless you're dropping just one
+ continue;
+ }
+ if (GridLayoutRule.sDebugGridLayout && (viewInfo.getName().equals(FQCN_SPACE)
+ || viewInfo.getName().equals(FQCN_SPACE_V7))) {
+ // In debug mode they might not be marked as hidden but we never never
+ // want to select these guys
+ continue;
+ }
+ newChildren.add(viewInfo);
+ }
+ }
+ boolean found = nodes.size() == newChildren.size();
+
+ if (found || newChildren.size() > 0) {
+ mCanvas.getSelectionManager().selectMultiple(newChildren);
+ }
+
+ return found;
+ }
+
+ /**
+ * Update the outline selection to select the given nodes, asynchronously.
+ * @param nodes The nodes to be selected
+ */
+ public void setOutlineSelection(final List<INode> nodes) {
+ Display.getDefault().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ selectDropped(nodes, null /* indices */);
+ syncOutlineSelection();
+ }
+ });
+ }
+
+ /**
+ * Syncs the current selection to the outline, synchronously.
+ */
+ public void syncOutlineSelection() {
+ OutlinePage outlinePage = mCanvas.getOutlinePage();
+ IWorkbenchPartSite site = outlinePage.getEditor().getSite();
+ ISelectionProvider selectionProvider = site.getSelectionProvider();
+ ISelection selection = selectionProvider.getSelection();
+ if (selection != null) {
+ outlinePage.setSelection(selection);
+ }
+ }
+
+ private void redraw() {
+ mCanvas.redraw();
+ }
+
+ SelectionItem createSelection(CanvasViewInfo vi) {
+ return new SelectionItem(mCanvas, vi);
+ }
+
+ /**
+ * Returns true if there is nothing selected
+ *
+ * @return true if there is nothing selected
+ */
+ public boolean isEmpty() {
+ return mSelections.size() == 0;
+ }
+
+ /**
+ * "Select" context menu which lists various menu options related to selection:
+ * <ul>
+ * <li> Select All
+ * <li> Select Parent
+ * <li> Select None
+ * <li> Select Siblings
+ * <li> Select Same Type
+ * </ul>
+ * etc.
+ */
+ public static class SelectionMenu extends SubmenuAction {
+ private final GraphicalEditorPart mEditor;
+
+ public SelectionMenu(GraphicalEditorPart editor) {
+ super("Select");
+ mEditor = editor;
+ }
+
+ @Override
+ public String getId() {
+ return "-selectionmenu"; //$NON-NLS-1$
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ LayoutCanvas canvas = mEditor.getCanvasControl();
+ SelectionManager selectionManager = canvas.getSelectionManager();
+ List<SelectionItem> selections = selectionManager.getSelections();
+ boolean selectedOne = selections.size() == 1;
+ boolean notRoot = selectedOne && !selections.get(0).isRoot();
+ boolean haveSelection = selections.size() > 0;
+
+ Action a;
+ a = selectionManager.new SelectAction("Select Parent\tEsc", SELECT_PARENT);
+ new ActionContributionItem(a).fill(menu, -1);
+ a.setEnabled(notRoot);
+ a.setAccelerator(SWT.ESC);
+
+ a = selectionManager.new SelectAction("Select Siblings", SELECT_SIBLINGS);
+ new ActionContributionItem(a).fill(menu, -1);
+ a.setEnabled(notRoot);
+
+ a = selectionManager.new SelectAction("Select Same Type", SELECT_SAME_TYPE);
+ new ActionContributionItem(a).fill(menu, -1);
+ a.setEnabled(selectedOne);
+
+ new Separator().fill(menu, -1);
+
+ // Special case for Select All: Use global action
+ a = canvas.getSelectAllAction();
+ new ActionContributionItem(a).fill(menu, -1);
+ a.setEnabled(true);
+
+ a = selectionManager.new SelectAction("Deselect All", SELECT_NONE);
+ new ActionContributionItem(a).fill(menu, -1);
+ a.setEnabled(haveSelection);
+ }
+ }
+
+ private static final int SELECT_PARENT = 1;
+ private static final int SELECT_SIBLINGS = 2;
+ private static final int SELECT_SAME_TYPE = 3;
+ private static final int SELECT_NONE = 4; // SELECT_ALL is handled separately
+
+ private class SelectAction extends Action {
+ private final int mType;
+
+ public SelectAction(String title, int type) {
+ super(title, IAction.AS_PUSH_BUTTON);
+ mType = type;
+ }
+
+ @Override
+ public void run() {
+ switch (mType) {
+ case SELECT_NONE:
+ selectNone();
+ break;
+ case SELECT_PARENT:
+ selectParent();
+ break;
+ case SELECT_SAME_TYPE:
+ selectSameType();
+ break;
+ case SELECT_SIBLINGS:
+ selectSiblings();
+ break;
+ }
+
+ List<INode> nodes = new ArrayList<INode>();
+ for (SelectionItem item : getSelections()) {
+ nodes.add(item.getNode());
+ }
+ setOutlineSelection(nodes);
+ }
+ }
+
+ public Pair<SelectionItem, SelectionHandle> findHandle(ControlPoint controlPoint) {
+ if (!isEmpty()) {
+ LayoutPoint layoutPoint = controlPoint.toLayout();
+ int distance = (int) ((PIXEL_MARGIN + PIXEL_RADIUS) / mCanvas.getScale());
+
+ for (SelectionItem item : getSelections()) {
+ SelectionHandles handles = item.getSelectionHandles();
+ // See if it's over the selection handles
+ SelectionHandle handle = handles.findHandle(layoutPoint, distance);
+ if (handle != null) {
+ return Pair.of(item, handle);
+ }
+ }
+
+ }
+ return null;
+ }
+
+ /** Performs the default action provided by the currently selected view */
+ public void performDefaultAction() {
+ final List<SelectionItem> selections = getSelections();
+ if (selections.size() > 0) {
+ NodeProxy primary = selections.get(0).getNode();
+ if (primary != null) {
+ RulesEngine rulesEngine = mCanvas.getRulesEngine();
+ final String id = rulesEngine.callGetDefaultActionId(primary);
+ if (id == null) {
+ return;
+ }
+ final List<RuleAction> actions = rulesEngine.callGetContextMenu(primary);
+ if (actions == null) {
+ return;
+ }
+ RuleAction matching = null;
+ for (RuleAction a : actions) {
+ if (id.equals(a.getId())) {
+ matching = a;
+ break;
+ }
+ }
+ if (matching == null) {
+ return;
+ }
+ final List<INode> selectedNodes = new ArrayList<INode>();
+ for (SelectionItem item : selections) {
+ NodeProxy n = item.getNode();
+ if (n != null) {
+ selectedNodes.add(n);
+ }
+ }
+ final RuleAction action = matching;
+ mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(action.getTitle(),
+ new Runnable() {
+ @Override
+ public void run() {
+ action.getCallback().action(action, selectedNodes,
+ action.getId(), null);
+ LayoutCanvas canvas = mCanvas;
+ CanvasViewInfo root = canvas.getViewHierarchy().getRoot();
+ if (root != null) {
+ UiViewElementNode uiViewNode = root.getUiViewNode();
+ NodeFactory nodeFactory = canvas.getNodeFactory();
+ NodeProxy rootNode = nodeFactory.create(uiViewNode);
+ if (rootNode != null) {
+ rootNode.applyPendingChanges();
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ /** Performs renaming the selected views */
+ public void performRename() {
+ final List<SelectionItem> selections = getSelections();
+ if (selections.size() > 0) {
+ NodeProxy primary = selections.get(0).getNode();
+ if (primary != null) {
+ performRename(primary, selections);
+ }
+ }
+ }
+
+ /**
+ * Performs renaming the given node.
+ *
+ * @param primary the node to be renamed, or the primary node (to get the
+ * current value from if more than one node should be renamed)
+ * @param selections if not null, a list of nodes to apply the setting to
+ * (which should include the primary)
+ * @return the result of the renaming operation
+ */
+ @NonNull
+ public RenameResult performRename(
+ final @NonNull INode primary,
+ final @Nullable List<SelectionItem> selections) {
+ String id = primary.getStringAttr(ANDROID_URI, ATTR_ID);
+ if (id != null && !id.isEmpty()) {
+ RenameResult result = RenameResourceWizard.renameResource(
+ mCanvas.getShell(),
+ mCanvas.getEditorDelegate().getGraphicalEditor().getProject(),
+ ResourceType.ID, BaseViewRule.stripIdPrefix(id), null, true /*canClear*/);
+ if (result.isCanceled()) {
+ return result;
+ } else if (!result.isUnavailable()) {
+ return result;
+ }
+ }
+ String currentId = primary.getStringAttr(ANDROID_URI, ATTR_ID);
+ currentId = BaseViewRule.stripIdPrefix(currentId);
+ InputDialog d = new InputDialog(
+ AdtPlugin.getDisplay().getActiveShell(),
+ "Set ID",
+ "New ID:",
+ currentId,
+ ResourceNameValidator.create(false, (IProject) null, ResourceType.ID));
+ if (d.open() == Window.OK) {
+ final String s = d.getValue();
+ mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel("Set ID",
+ new Runnable() {
+ @Override
+ public void run() {
+ String newId = s;
+ newId = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(s);
+ if (selections != null) {
+ for (SelectionItem item : selections) {
+ NodeProxy node = item.getNode();
+ if (node != null) {
+ node.setAttribute(ANDROID_URI, ATTR_ID, newId);
+ }
+ }
+ } else {
+ primary.setAttribute(ANDROID_URI, ATTR_ID, newId);
+ }
+
+ LayoutCanvas canvas = mCanvas;
+ CanvasViewInfo root = canvas.getViewHierarchy().getRoot();
+ if (root != null) {
+ UiViewElementNode uiViewNode = root.getUiViewNode();
+ NodeFactory nodeFactory = canvas.getNodeFactory();
+ NodeProxy rootNode = nodeFactory.create(uiViewNode);
+ if (rootNode != null) {
+ rootNode.applyPendingChanges();
+ }
+ }
+ }
+ });
+ return RenameResult.name(BaseViewRule.stripIdPrefix(s));
+ } else {
+ return RenameResult.canceled();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java
new file mode 100644
index 000000000..97d048108
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2010 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.ide.common.api.DrawingStyle;
+import com.android.ide.common.api.IGraphics;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Margins;
+import com.android.ide.common.api.Rect;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+
+import org.eclipse.swt.graphics.GC;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The {@link SelectionOverlay} paints the current selection as an overlay.
+ */
+public class SelectionOverlay extends Overlay {
+ private final LayoutCanvas mCanvas;
+ private boolean mHidden;
+
+ /**
+ * Constructs a new {@link SelectionOverlay} tied to the given canvas.
+ *
+ * @param canvas the associated canvas
+ */
+ public SelectionOverlay(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ }
+
+ /**
+ * Set whether the selection overlay should be hidden. This is done during some
+ * gestures like resize where the new bounds could be confused with the current
+ * selection bounds.
+ *
+ * @param hidden when true, hide the selection bounds, when false, unhide.
+ */
+ public void setHidden(boolean hidden) {
+ mHidden = hidden;
+ }
+
+ /**
+ * Paints the selection.
+ *
+ * @param selectionManager The {@link SelectionManager} holding the
+ * selection.
+ * @param gcWrapper The graphics context wrapper for the layout rules to use.
+ * @param gc The SWT graphics object
+ * @param rulesEngine The {@link RulesEngine} holding the rules.
+ */
+ public void paint(SelectionManager selectionManager, GCWrapper gcWrapper,
+ GC gc, RulesEngine rulesEngine) {
+ if (mHidden) {
+ return;
+ }
+
+ List<SelectionItem> selections = selectionManager.getSelections();
+ int n = selections.size();
+ if (n > 0) {
+ List<NodeProxy> selectedNodes = new ArrayList<NodeProxy>();
+ boolean isMultipleSelection = n > 1;
+ for (SelectionItem s : selections) {
+ if (s.isRoot()) {
+ // The root selection is never painted
+ continue;
+ }
+
+ NodeProxy node = s.getNode();
+ if (node != null) {
+ paintSelection(gcWrapper, gc, s, isMultipleSelection);
+ selectedNodes.add(node);
+ }
+ }
+
+ if (selectedNodes.size() > 0) {
+ paintSelectionFeedback(gcWrapper, selectedNodes, rulesEngine);
+ } else {
+ CanvasViewInfo root = mCanvas.getViewHierarchy().getRoot();
+ if (root != null) {
+ NodeProxy parent = mCanvas.getNodeFactory().create(root);
+ rulesEngine.callPaintSelectionFeedback(gcWrapper,
+ parent, Collections.<INode>emptyList(), root.getViewObject());
+ }
+ }
+
+ if (n == 1) {
+ NodeProxy node = selections.get(0).getNode();
+ if (node != null) {
+ paintHints(gcWrapper, node, rulesEngine);
+ }
+ }
+ } else {
+ CanvasViewInfo root = mCanvas.getViewHierarchy().getRoot();
+ if (root != null) {
+ NodeProxy parent = mCanvas.getNodeFactory().create(root);
+ rulesEngine.callPaintSelectionFeedback(gcWrapper,
+ parent, Collections.<INode>emptyList(), root.getViewObject());
+ }
+ }
+ }
+
+ /** Paint hint for current selection */
+ private void paintHints(GCWrapper gcWrapper, NodeProxy node, RulesEngine rulesEngine) {
+ INode parent = node.getParent();
+ if (parent instanceof NodeProxy) {
+ NodeProxy parentNode = (NodeProxy) parent;
+ List<String> infos = rulesEngine.callGetSelectionHint(parentNode, node);
+ if (infos != null && infos.size() > 0) {
+ gcWrapper.useStyle(DrawingStyle.HELP);
+
+ Rect b = mCanvas.getImageOverlay().getImageBounds();
+ if (b == null) {
+ return;
+ }
+
+ // Compute the location to display the help. This is done in
+ // layout coordinates, so we need to apply the scale in reverse
+ // when making pixel margins
+ // TODO: We could take the Canvas dimensions into account to see
+ // where there is more room.
+ // TODO: The scrollbars should take the presence of hint text
+ // into account.
+ double scale = mCanvas.getScale();
+ int x, y;
+ if (b.w > b.h) {
+ x = (int) (b.x + 3 / scale);
+ y = (int) (b.y + b.h + 6 / scale);
+ } else {
+ x = (int) (b.x + b.w + 6 / scale);
+ y = (int) (b.y + 3 / scale);
+ }
+ gcWrapper.drawBoxedStrings(x, y, infos);
+ }
+ }
+ }
+
+ private void paintSelectionFeedback(GCWrapper gcWrapper, List<NodeProxy> nodes,
+ RulesEngine rulesEngine) {
+ // Add fastpath for n=1
+
+ // Group nodes into parent/child groups
+ Set<INode> parents = new HashSet<INode>();
+ for (INode node : nodes) {
+ INode parent = node.getParent();
+ if (/*parent == null || */parent instanceof NodeProxy) {
+ NodeProxy parentNode = (NodeProxy) parent;
+ parents.add(parentNode);
+ }
+ }
+ ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+ for (INode parent : parents) {
+ List<INode> children = new ArrayList<INode>();
+ for (INode node : nodes) {
+ INode nodeParent = node.getParent();
+ if (nodeParent == parent) {
+ children.add(node);
+ }
+ }
+ CanvasViewInfo viewInfo = viewHierarchy.findViewInfoFor((NodeProxy) parent);
+ Object view = viewInfo != null ? viewInfo.getViewObject() : null;
+
+ rulesEngine.callPaintSelectionFeedback(gcWrapper,
+ (NodeProxy) parent, children, view);
+ }
+ }
+
+ /** Called by the canvas when a view is being selected. */
+ private void paintSelection(IGraphics gc, GC swtGc, SelectionItem item,
+ boolean isMultipleSelection) {
+ CanvasViewInfo view = item.getViewInfo();
+ if (view.isHidden()) {
+ return;
+ }
+
+ NodeProxy selectedNode = item.getNode();
+ Rect r = selectedNode.getBounds();
+ if (!r.isValid()) {
+ return;
+ }
+
+ gc.useStyle(DrawingStyle.SELECTION);
+
+ Margins insets = mCanvas.getInsets(selectedNode.getFqcn());
+ int x1 = r.x;
+ int y1 = r.y;
+ int x2 = r.x2() + 1;
+ int y2 = r.y2() + 1;
+
+ if (insets != null) {
+ x1 += insets.left;
+ x2 -= insets.right;
+ y1 += insets.top;
+ y2 -= insets.bottom;
+ }
+
+ gc.drawRect(x1, y1, x2, y2);
+
+ // Paint sibling rectangles, if applicable
+ List<CanvasViewInfo> siblings = view.getNodeSiblings();
+ if (siblings != null) {
+ for (CanvasViewInfo sibling : siblings) {
+ if (sibling != view) {
+ r = SwtUtils.toRect(sibling.getSelectionRect());
+ gc.fillRect(r);
+ gc.drawRect(r);
+ }
+ }
+ }
+
+ // Paint selection handles. These are painted in control coordinates on the
+ // real SWT GC object rather than in layout coordinates on the GCWrapper,
+ // since we want them to have a fixed size that is independent of the
+ // screen zoom.
+ CanvasTransform horizontalTransform = mCanvas.getHorizontalTransform();
+ CanvasTransform verticalTransform = mCanvas.getVerticalTransform();
+ int radius = SelectionHandle.PIXEL_RADIUS;
+ int doubleRadius = 2 * radius;
+ for (SelectionHandle handle : item.getSelectionHandles()) {
+ int cx = horizontalTransform.translate(handle.centerX);
+ int cy = verticalTransform.translate(handle.centerY);
+
+ SwtDrawingStyle style = SwtDrawingStyle.of(DrawingStyle.SELECTION);
+ gc.setAlpha(style.getStrokeAlpha());
+ swtGc.fillRectangle(cx - radius, cy - radius, doubleRadius, doubleRadius);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ShowWithinMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ShowWithinMenu.java
new file mode 100644
index 000000000..d1d529e5a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ShowWithinMenu.java
@@ -0,0 +1,82 @@
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.swt.widgets.Menu;
+
+import java.util.List;
+
+/**
+ * Action which creates a submenu for the "Show Included In" action
+ */
+class ShowWithinMenu extends SubmenuAction {
+ private LayoutEditorDelegate mEditorDelegate;
+
+ ShowWithinMenu(LayoutEditorDelegate editorDelegate) {
+ super("Show Included In");
+ mEditorDelegate = editorDelegate;
+ }
+
+ @Override
+ protected void addMenuItems(Menu menu) {
+ GraphicalEditorPart graphicalEditor = mEditorDelegate.getGraphicalEditor();
+ IFile file = graphicalEditor.getEditedFile();
+ if (graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) {
+ IProject project = file.getProject();
+ IncludeFinder finder = IncludeFinder.get(project);
+ final List<Reference> includedBy = finder.getIncludedBy(file);
+
+ if (includedBy != null && includedBy.size() > 0) {
+ for (final Reference reference : includedBy) {
+ String title = reference.getDisplayName();
+ IAction action = new ShowWithinAction(title, reference);
+ new ActionContributionItem(action).fill(menu, -1);
+ }
+ new Separator().fill(menu, -1);
+ }
+ IAction action = new ShowWithinAction("Nothing", null);
+ if (includedBy == null || includedBy.size() == 0) {
+ action.setEnabled(false);
+ }
+ new ActionContributionItem(action).fill(menu, -1);
+ } else {
+ addDisabledMessageItem("Not supported on platform");
+ }
+ }
+
+ /** Action to select one particular include-context */
+ private class ShowWithinAction extends Action {
+ private Reference mReference;
+
+ public ShowWithinAction(String title, Reference reference) {
+ super(title, IAction.AS_RADIO_BUTTON);
+ mReference = reference;
+ }
+
+ @Override
+ public boolean isChecked() {
+ Reference within = mEditorDelegate.getGraphicalEditor().getIncludedWithin();
+ if (within == null) {
+ return mReference == null;
+ } else {
+ return within.equals(mReference);
+ }
+ }
+
+ @Override
+ public void run() {
+ if (!isChecked()) {
+ mEditorDelegate.getGraphicalEditor().showIn(mReference);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java
new file mode 100644
index 000000000..198c16484
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java
@@ -0,0 +1,124 @@
+/*
+ * 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.annotations.NonNull;
+import com.android.ide.common.api.INode.IAttribute;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Represents one XML attribute in a {@link SimpleElement}.
+ * <p/>
+ * The attribute is always represented by a namespace URI, a name and a value.
+ * The name cannot be empty.
+ * The namespace URI can be empty for an attribute without a namespace but is never null.
+ * The value can be empty but cannot be null.
+ * <p/>
+ * For a more detailed explanation of the purpose of this class,
+ * please see {@link SimpleXmlTransfer}.
+ */
+public class SimpleAttribute implements IAttribute {
+ private final String mName;
+ private final String mValue;
+ private final String mUri;
+
+ /**
+ * Creates a new {@link SimpleAttribute}.
+ * <p/>
+ * Any null value will be converted to an empty non-null string.
+ * However it is a semantic error to use an empty name -- no assertion is done though.
+ *
+ * @param uri The URI of the attribute.
+ * @param name The XML local name of the attribute.
+ * @param value The value of the attribute.
+ */
+ public SimpleAttribute(String uri, String name, String value) {
+ mUri = uri == null ? "" : uri;
+ mName = name == null ? "" : name;
+ mValue = value == null ? "" : value;
+ }
+
+ /**
+ * Returns the namespace URI of the attribute.
+ * Can be empty for an attribute without a namespace but is never null.
+ */
+ @Override
+ public @NonNull String getUri() {
+ return mUri;
+ }
+
+ /** Returns the XML local name of the attribute. Cannot be null nor empty. */
+ @Override
+ public @NonNull String getName() {
+ return mName;
+ }
+
+ /** Returns the value of the attribute. Cannot be null. Can be empty. */
+ @Override
+ public @NonNull String getValue() {
+ return mValue;
+ }
+
+ // reader and writer methods
+
+ @Override
+ public String toString() {
+ return String.format("@%s:%s=%s\n", //$NON-NLS-1$
+ mName,
+ mUri,
+ mValue);
+ }
+
+ private static final Pattern REGEXP =
+ Pattern.compile("[^@]*@([^:]+):([^=]*)=([^\n]*)\n*"); //$NON-NLS-1$
+
+ static SimpleAttribute parseString(String value) {
+ Matcher m = REGEXP.matcher(value);
+ if (m.matches()) {
+ return new SimpleAttribute(m.group(2), m.group(1), m.group(3));
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof SimpleAttribute) {
+ SimpleAttribute sa = (SimpleAttribute) obj;
+
+ return mName.equals(sa.mName) &&
+ mUri.equals(sa.mUri) &&
+ mValue.equals(sa.mValue);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ long c = mName.hashCode();
+ // uses the formula defined in java.util.List.hashCode()
+ c = 31*c + mUri.hashCode();
+ c = 31*c + mValue.hashCode();
+ if (c > 0x0FFFFFFFFL) {
+ // wrap any overflow
+ c = c ^ (c >> 32);
+ }
+ return (int)(c & 0x0FFFFFFFFL);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java
new file mode 100644
index 000000000..9acc8c25e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java
@@ -0,0 +1,370 @@
+/*
+ * 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.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.Rect;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents an XML element with a name, attributes and inner elements.
+ * <p/>
+ * The semantic of the element name is to be a fully qualified class name of a View to inflate.
+ * The element name is not expected to have a name space.
+ * <p/>
+ * For a more detailed explanation of the purpose of this class,
+ * please see {@link SimpleXmlTransfer}.
+ */
+public class SimpleElement implements IDragElement {
+
+ /** Version number of the internal serialized string format. */
+ private static final String FORMAT_VERSION = "3";
+
+ private final String mFqcn;
+ private final String mParentFqcn;
+ private final Rect mBounds;
+ private final Rect mParentBounds;
+ private final List<IDragAttribute> mAttributes = new ArrayList<IDragAttribute>();
+ private final List<IDragElement> mElements = new ArrayList<IDragElement>();
+
+ private IDragAttribute[] mCachedAttributes = null;
+ private IDragElement[] mCachedElements = null;
+ private SelectionItem mSelectionItem;
+
+ /**
+ * Creates a new {@link SimpleElement} with the specified element name.
+ *
+ * @param fqcn A fully qualified class name of a View to inflate, e.g.
+ * "android.view.Button". Must not be null nor empty.
+ * @param parentFqcn The fully qualified class name of the parent of this element.
+ * Can be null but not empty.
+ * @param bounds The canvas bounds of the originating canvas node of the element.
+ * If null, a non-null invalid rectangle will be assigned.
+ * @param parentBounds The canvas bounds of the parent of this element. Can be null.
+ */
+ public SimpleElement(String fqcn, String parentFqcn, Rect bounds, Rect parentBounds) {
+ mFqcn = fqcn;
+ mParentFqcn = parentFqcn;
+ mBounds = bounds == null ? new Rect() : bounds.copy();
+ mParentBounds = parentBounds == null ? new Rect() : parentBounds.copy();
+ }
+
+ /**
+ * Returns the element name, which must match a fully qualified class name of
+ * a View to inflate.
+ */
+ @Override
+ public @NonNull String getFqcn() {
+ return mFqcn;
+ }
+
+ /**
+ * Returns the bounds of the element's node, if it originated from an existing
+ * canvas. The rectangle is invalid and non-null when the element originated
+ * from the object palette (unless it successfully rendered a preview)
+ */
+ @Override
+ public @NonNull Rect getBounds() {
+ return mBounds;
+ }
+
+ /**
+ * Returns the fully qualified class name of the parent, if the element originated
+ * from an existing canvas. Returns null if the element has no parent, such as a top
+ * level element or an element originating from the object palette.
+ */
+ @Override
+ public String getParentFqcn() {
+ return mParentFqcn;
+ }
+
+ /**
+ * Returns the bounds of the element's parent, absolute for the canvas, or null if there
+ * is no suitable parent. This is null when {@link #getParentFqcn()} is null.
+ */
+ @Override
+ public @NonNull Rect getParentBounds() {
+ return mParentBounds;
+ }
+
+ @Override
+ public @NonNull IDragAttribute[] getAttributes() {
+ if (mCachedAttributes == null) {
+ mCachedAttributes = mAttributes.toArray(new IDragAttribute[mAttributes.size()]);
+ }
+ return mCachedAttributes;
+ }
+
+ @Override
+ public IDragAttribute getAttribute(@Nullable String uri, @NonNull String localName) {
+ for (IDragAttribute attr : mAttributes) {
+ if (attr.getUri().equals(uri) && attr.getName().equals(localName)) {
+ return attr;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public @NonNull IDragElement[] getInnerElements() {
+ if (mCachedElements == null) {
+ mCachedElements = mElements.toArray(new IDragElement[mElements.size()]);
+ }
+ return mCachedElements;
+ }
+
+ public void addAttribute(SimpleAttribute attr) {
+ mCachedAttributes = null;
+ mAttributes.add(attr);
+ }
+
+ public void addInnerElement(SimpleElement e) {
+ mCachedElements = null;
+ mElements.add(e);
+ }
+
+ @Override
+ public boolean isSame(@NonNull INode node) {
+ if (mSelectionItem != null) {
+ return node == mSelectionItem.getNode();
+ } else {
+ return node.getBounds().equals(mBounds);
+ }
+ }
+
+ void setSelectionItem(@Nullable SelectionItem selectionItem) {
+ mSelectionItem = selectionItem;
+ }
+
+ @Nullable
+ SelectionItem getSelectionItem() {
+ return mSelectionItem;
+ }
+
+ @Nullable
+ static SimpleElement findPrimary(SimpleElement[] elements, SelectionItem primary) {
+ if (elements == null || elements.length == 0) {
+ return null;
+ }
+
+ if (elements.length == 1 || primary == null) {
+ return elements[0];
+ }
+
+ for (SimpleElement element : elements) {
+ if (element.getSelectionItem() == primary) {
+ return element;
+ }
+ }
+
+ return elements[0];
+ }
+
+ // reader and writer methods
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{V=").append(FORMAT_VERSION);
+ sb.append(",N=").append(mFqcn);
+ if (mParentFqcn != null) {
+ sb.append(",P=").append(mParentFqcn);
+ }
+ if (mBounds != null && mBounds.isValid()) {
+ sb.append(String.format(",R=%d %d %d %d", mBounds.x, mBounds.y, mBounds.w, mBounds.h));
+ }
+ if (mParentBounds != null && mParentBounds.isValid()) {
+ sb.append(String.format(",Q=%d %d %d %d",
+ mParentBounds.x, mParentBounds.y, mParentBounds.w, mParentBounds.h));
+ }
+ sb.append('\n');
+ for (IDragAttribute a : mAttributes) {
+ sb.append(a.toString());
+ }
+ for (IDragElement e : mElements) {
+ sb.append(e.toString());
+ }
+ sb.append("}\n"); //$NON-NLS-1$
+ return sb.toString();
+ }
+
+ /** Parses a string containing one or more elements. */
+ static SimpleElement[] parseString(String value) {
+ ArrayList<SimpleElement> elements = new ArrayList<SimpleElement>();
+ String[] lines = value.split("\n");
+ int[] index = new int[] { 0 };
+ SimpleElement element = null;
+ while ((element = parseLines(lines, index)) != null) {
+ elements.add(element);
+ }
+ return elements.toArray(new SimpleElement[elements.size()]);
+ }
+
+ /**
+ * Parses one element from the input lines array, starting at the inOutIndex
+ * and updating the inOutIndex to match the next unread line on output.
+ */
+ private static SimpleElement parseLines(String[] lines, int[] inOutIndex) {
+ SimpleElement e = null;
+ int index = inOutIndex[0];
+ while (index < lines.length) {
+ String line = lines[index++];
+ String s = line.trim();
+ if (s.startsWith("{")) { //$NON-NLS-1$
+ if (e == null) {
+ // This is the element's header, it should have
+ // the format "key=value,key=value,..."
+ String version = null;
+ String fqcn = null;
+ String parent = null;
+ Rect bounds = null;
+ Rect pbounds = null;
+
+ for (String s2 : s.substring(1).split(",")) { //$NON-NLS-1$
+ int pos = s2.indexOf('=');
+ if (pos <= 0 || pos == s2.length() - 1) {
+ continue;
+ }
+ String key = s2.substring(0, pos).trim();
+ String value = s2.substring(pos + 1).trim();
+
+ if (key.equals("V")) { //$NON-NLS-1$
+ version = value;
+ if (!value.equals(FORMAT_VERSION)) {
+ // Wrong format version. Don't even try to process anything
+ // else and just give up everything.
+ inOutIndex[0] = index;
+ return null;
+ }
+
+ } else if (key.equals("N")) { //$NON-NLS-1$
+ fqcn = value;
+
+ } else if (key.equals("P")) { //$NON-NLS-1$
+ parent = value;
+
+ } else if (key.equals("R") || key.equals("Q")) { //$NON-NLS-1$ //$NON-NLS-2$
+ // Parse the canvas bounds
+ String[] sb = value.split(" +"); //$NON-NLS-1$
+ if (sb != null && sb.length == 4) {
+ Rect r = null;
+ try {
+ r = new Rect();
+ r.x = Integer.parseInt(sb[0]);
+ r.y = Integer.parseInt(sb[1]);
+ r.w = Integer.parseInt(sb[2]);
+ r.h = Integer.parseInt(sb[3]);
+
+ if (key.equals("R")) {
+ bounds = r;
+ } else {
+ pbounds = r;
+ }
+ } catch (NumberFormatException ignore) {
+ }
+ }
+ }
+ }
+
+ // We need at least a valid name to recreate an element
+ if (version != null && fqcn != null && fqcn.length() > 0) {
+ e = new SimpleElement(fqcn, parent, bounds, pbounds);
+ }
+ } else {
+ // This is an inner element... need to parse the { line again.
+ inOutIndex[0] = index - 1;
+ SimpleElement e2 = SimpleElement.parseLines(lines, inOutIndex);
+ if (e2 != null) {
+ e.addInnerElement(e2);
+ }
+ index = inOutIndex[0];
+ }
+
+ } else if (e != null && s.startsWith("@")) { //$NON-NLS-1$
+ SimpleAttribute a = SimpleAttribute.parseString(line);
+ if (a != null) {
+ e.addAttribute(a);
+ }
+
+ } else if (e != null && s.startsWith("}")) { //$NON-NLS-1$
+ // We're done with this element
+ inOutIndex[0] = index;
+ return e;
+ }
+ }
+ inOutIndex[0] = index;
+ return null;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof SimpleElement) {
+ SimpleElement se = (SimpleElement) obj;
+
+ // Bounds and parentFqcn must be null on both sides or equal.
+ if ((mBounds == null && se.mBounds != null) ||
+ (mBounds != null && !mBounds.equals(se.mBounds))) {
+ return false;
+ }
+ if ((mParentFqcn == null && se.mParentFqcn != null) ||
+ (mParentFqcn != null && !mParentFqcn.equals(se.mParentFqcn))) {
+ return false;
+ }
+ if ((mParentBounds == null && se.mParentBounds != null) ||
+ (mParentBounds != null && !mParentBounds.equals(se.mParentBounds))) {
+ return false;
+ }
+
+ return mFqcn.equals(se.mFqcn) &&
+ mAttributes.size() == se.mAttributes.size() &&
+ mElements.size() == se.mElements.size() &&
+ mAttributes.equals(se.mAttributes) &&
+ mElements.equals(se.mElements);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ long c = mFqcn.hashCode();
+ // uses the formula defined in java.util.List.hashCode()
+ c = 31*c + mAttributes.hashCode();
+ c = 31*c + mElements.hashCode();
+ if (mParentFqcn != null) {
+ c = 31*c + mParentFqcn.hashCode();
+ }
+ if (mBounds != null && mBounds.isValid()) {
+ c = 31*c + mBounds.hashCode();
+ }
+ if (mParentBounds != null && mParentBounds.isValid()) {
+ c = 31*c + mParentBounds.hashCode();
+ }
+
+ if (c > 0x0FFFFFFFFL) {
+ // wrap any overflow
+ c = c ^ (c >> 32);
+ }
+ return (int)(c & 0x0FFFFFFFFL);
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleXmlTransfer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleXmlTransfer.java
new file mode 100644
index 000000000..20ac2033e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleXmlTransfer.java
@@ -0,0 +1,154 @@
+/*
+ * 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.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+
+import org.eclipse.swt.dnd.ByteArrayTransfer;
+import org.eclipse.swt.dnd.TransferData;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * A d'n'd {@link Transfer} class that can transfer a <em>simplified</em> XML fragment
+ * to transfer elements and their attributes between {@link LayoutCanvas}.
+ * <p/>
+ * The implementation is based on the {@link ByteArrayTransfer} and what we transfer
+ * is text with the following fixed format:
+ * <p/>
+ * <pre>
+ * {element-name element-property ...
+ * attrib_name="attrib_value"
+ * attrib2="..."
+ * {...inner elements...
+ * }
+ * }
+ * {...next element...
+ * }
+ *
+ * </pre>
+ * The format has nothing to do with XML per se, except for the fact that the
+ * transfered content represents XML elements and XML attributes.
+ *
+ * <p/>
+ * The detailed syntax is:
+ * <pre>
+ * - ELEMENT := {NAME PROPERTY*\nATTRIB_LINE*ELEMENT*}\n
+ * - PROPERTY := $[A-Z]=[^ ]*
+ * - NAME := [^\n=]+
+ * - ATTRIB_LINE := @URI:NAME=[^\n]*\n
+ * </pre>
+ *
+ * Elements are represented by {@link SimpleElement}s and their attributes by
+ * {@link SimpleAttribute}s, all of which have very specific properties that are
+ * specifically limited to our needs for drag'n'drop.
+ */
+final class SimpleXmlTransfer extends ByteArrayTransfer {
+
+ // Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html
+
+ private static final String TYPE_NAME = "android.ADT.simple.xml.transfer.1"; //$NON-NLS-1$
+ private static final int TYPE_ID = registerType(TYPE_NAME);
+ private static final SimpleXmlTransfer sInstance = new SimpleXmlTransfer();
+
+ /** Private constructor. Use {@link #getInstance()} to retrieve the singleton instance. */
+ private SimpleXmlTransfer() {
+ // pass
+ }
+
+ /** Returns the singleton instance. */
+ public static SimpleXmlTransfer getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * Helper method that returns the FQCN transfered for the given {@link ElementDescriptor}.
+ * <p/>
+ * If the descriptor is a {@link ViewElementDescriptor}, the transfered data is the FQCN
+ * of the Android View class represented (e.g. "android.widget.Button").
+ * For any other non-null descriptor, the XML name is used.
+ * Otherwise it is null.
+ *
+ * @param desc The {@link ElementDescriptor} to transfer.
+ * @return The FQCN, XML name or null.
+ */
+ public static String getFqcn(ElementDescriptor desc) {
+ if (desc instanceof ViewElementDescriptor) {
+ return ((ViewElementDescriptor) desc).getFullClassName();
+ } else if (desc != null) {
+ return desc.getXmlName();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected int[] getTypeIds() {
+ return new int[] { TYPE_ID };
+ }
+
+ @Override
+ protected String[] getTypeNames() {
+ return new String[] { TYPE_NAME };
+ }
+
+ /** Transforms a array of {@link SimpleElement} into a native data transfer. */
+ @Override
+ protected void javaToNative(Object object, TransferData transferData) {
+ if (object == null || !(object instanceof SimpleElement[])) {
+ return;
+ }
+
+ if (isSupportedType(transferData)) {
+ StringBuilder sb = new StringBuilder();
+ for (SimpleElement e : (SimpleElement[]) object) {
+ sb.append(e.toString());
+ }
+ String data = sb.toString();
+
+ try {
+ byte[] buf = data.getBytes("UTF-8"); //$NON-NLS-1$
+ super.javaToNative(buf, transferData);
+ } catch (UnsupportedEncodingException e) {
+ // unlikely; ignore
+ }
+ }
+ }
+
+ /**
+ * Recreates an array of {@link SimpleElement} from a native data transfer.
+ *
+ * @return An array of {@link SimpleElement} or null. The array may be empty.
+ */
+ @Override
+ protected Object nativeToJava(TransferData transferData) {
+ if (isSupportedType(transferData)) {
+ byte[] buf = (byte[]) super.nativeToJava(transferData);
+ if (buf != null && buf.length > 0) {
+ try {
+ String s = new String(buf, "UTF-8"); //$NON-NLS-1$
+ return SimpleElement.parseString(s);
+ } catch (UnsupportedEncodingException e) {
+ // unlikely to happen, but still possible
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SubmenuAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SubmenuAction.java
new file mode 100644
index 000000000..0923dda79
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SubmenuAction.java
@@ -0,0 +1,75 @@
+
+package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ActionContributionItem;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IMenuCreator;
+import org.eclipse.swt.events.MenuEvent;
+import org.eclipse.swt.events.MenuListener;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+
+/**
+ * Action which creates a submenu that is dynamically populated by subclasses
+ */
+public abstract class SubmenuAction extends Action implements MenuListener, IMenuCreator {
+ private Menu mMenu;
+
+ public SubmenuAction(String title) {
+ super(title, IAction.AS_DROP_DOWN_MENU);
+ }
+
+ @Override
+ public IMenuCreator getMenuCreator() {
+ return this;
+ }
+
+ @Override
+ public void dispose() {
+ if (mMenu != null) {
+ mMenu.dispose();
+ mMenu = null;
+ }
+ }
+
+ @Override
+ public Menu getMenu(Control parent) {
+ return null;
+ }
+
+ @Override
+ public Menu getMenu(Menu parent) {
+ mMenu = new Menu(parent);
+ mMenu.addMenuListener(this);
+ return mMenu;
+ }
+
+ @Override
+ public void menuHidden(MenuEvent e) {
+ }
+
+ protected abstract void addMenuItems(Menu menu);
+
+ @Override
+ public void menuShown(MenuEvent e) {
+ // TODO: Replace this stuff with manager.setRemoveAllWhenShown(true);
+ MenuItem[] menuItems = mMenu.getItems();
+ for (int i = 0; i < menuItems.length; i++) {
+ menuItems[i].dispose();
+ }
+ addMenuItems(mMenu);
+ }
+
+ protected void addDisabledMessageItem(String message) {
+ IAction action = new Action(message, IAction.AS_PUSH_BUTTON) {
+ @Override
+ public void run() {
+ }
+ };
+ action.setEnabled(false);
+ new ActionContributionItem(action).fill(mMenu, -1);
+
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java
new file mode 100644
index 000000000..93a33283c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2010 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.ide.common.api.DrawingStyle;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.RGB;
+
+/**
+ * Description of the drawing styles with specific color, line style and alpha
+ * definitions. This class corresponds to the more generic {@link DrawingStyle}
+ * class which defines the drawing styles but does not introduce any specific
+ * SWT values to the API clients.
+ * <p>
+ * TODO: This class should eventually be replaced by a scheme where the color
+ * constants are instead coming from the theme.
+ */
+public enum SwtDrawingStyle {
+ /**
+ * The style definition corresponding to {@link DrawingStyle#SELECTION}
+ */
+ SELECTION(new RGB(0x00, 0x99, 0xFF), 192, new RGB(0x00, 0x99, 0xFF), 192, 1, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#GUIDELINE}
+ */
+ GUIDELINE(new RGB(0x00, 0xAA, 0x00), 192, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#GUIDELINE}
+ */
+ GUIDELINE_SHADOW(new RGB(0x00, 0xAA, 0x00), 192, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#GUIDELINE_DASHED}
+ */
+ GUIDELINE_DASHED(new RGB(0x00, 0xAA, 0x00), 192, SWT.LINE_CUSTOM),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#DISTANCE}
+ */
+ DISTANCE(new RGB(0xFF, 0x00, 0x00), 192 - 32, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#GRID}
+ */
+ GRID(new RGB(0xAA, 0xAA, 0xAA), 128, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#HOVER}
+ */
+ HOVER(null, 0, new RGB(0xFF, 0xFF, 0xFF), 40, 1, SWT.LINE_DOT),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#HOVER}
+ */
+ HOVER_SELECTION(null, 0, new RGB(0xFF, 0xFF, 0xFF), 10, 1, SWT.LINE_DOT),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#ANCHOR}
+ */
+ ANCHOR(new RGB(0x00, 0x99, 0xFF), 96, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#OUTLINE}
+ */
+ OUTLINE(new RGB(0x88, 0xFF, 0x88), 160, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#DROP_RECIPIENT}
+ */
+ DROP_RECIPIENT(new RGB(0xFF, 0x99, 0x00), 255, new RGB(0xFF, 0x99, 0x00), 160, 2,
+ SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#DROP_ZONE}
+ */
+ DROP_ZONE(new RGB(0x00, 0xAA, 0x00), 220, new RGB(0x55, 0xAA, 0x00), 200, 1, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to
+ * {@link DrawingStyle#DROP_ZONE_ACTIVE}
+ */
+ DROP_ZONE_ACTIVE(new RGB(0x00, 0xAA, 0x00), 220, new RGB(0x00, 0xAA, 0x00), 128, 2,
+ SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#DROP_PREVIEW}
+ */
+ DROP_PREVIEW(new RGB(0xFF, 0x99, 0x00), 255, null, 0, 2, SWT.LINE_CUSTOM),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#RESIZE_PREVIEW}
+ */
+ RESIZE_PREVIEW(new RGB(0xFF, 0x99, 0x00), 255, null, 0, 2, SWT.LINE_SOLID),
+
+ /**
+ * The style used to show a proposed resize bound which is being rejected (for example,
+ * because there is no near edge to attach to in a RelativeLayout).
+ */
+ RESIZE_FAIL(new RGB(0xFF, 0x99, 0x00), 255, null, 0, 2, SWT.LINE_CUSTOM),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#HELP}
+ */
+ HELP(new RGB(0xFF, 0xFF, 0xFF), 255, new RGB(0x00, 0x00, 0x00), 128, 1, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#INVALID}
+ */
+ INVALID(new RGB(0xFF, 0xFF, 0xFF), 255, new RGB(0xFF, 0x00, 0x00), 64, 2, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#DEPENDENCY}
+ */
+ DEPENDENCY(new RGB(0xFF, 0xFF, 0xFF), 255, new RGB(0xFF, 0xFF, 0x00), 24, 2, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#CYCLE}
+ */
+ CYCLE(new RGB(0xFF, 0x00, 0x00), 192, null, 0, 1, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#DRAGGED}
+ */
+ DRAGGED(new RGB(0xFF, 0xFF, 0xFF), 255, new RGB(0x00, 0xFF, 0x00), 16, 2, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#EMPTY}
+ */
+ EMPTY(new RGB(0xFF, 0xFF, 0x55), 255, new RGB(0xFF, 0xFF, 0x55), 255, 1, SWT.LINE_DASH),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#CUSTOM1}
+ */
+ CUSTOM1(new RGB(0xFF, 0x00, 0xFF), 255, null, 0, 1, SWT.LINE_SOLID),
+
+ /**
+ * The style definition corresponding to {@link DrawingStyle#CUSTOM2}
+ */
+ CUSTOM2(new RGB(0x00, 0xFF, 0xFF), 255, null, 0, 1, SWT.LINE_DOT);
+
+ /**
+ * Construct a new style value with the given foreground, background, width,
+ * linestyle and transparency.
+ *
+ * @param stroke A color descriptor for the foreground color, or null if no
+ * foreground color should be set
+ * @param fill A color descriptor for the background color, or null if no
+ * foreground color should be set
+ * @param lineWidth The line width, in pixels, or 0 if no line width should
+ * be set
+ * @param lineStyle The SWT line style - such as {@link SWT#LINE_SOLID}.
+ * @param strokeAlpha The alpha value of the stroke, an integer in the range 0 to 255
+ * where 0 is fully transparent and 255 is fully opaque.
+ * @param fillAlpha The alpha value of the fill, an integer in the range 0 to 255
+ * where 0 is fully transparent and 255 is fully opaque.
+ */
+ private SwtDrawingStyle(RGB stroke, int strokeAlpha, RGB fill, int fillAlpha, int lineWidth,
+ int lineStyle) {
+ mStroke = stroke;
+ mFill = fill;
+ mLineWidth = lineWidth;
+ mLineStyle = lineStyle;
+ mStrokeAlpha = strokeAlpha;
+ mFillAlpha = fillAlpha;
+ }
+
+ /**
+ * Convenience constructor for typical drawing styles, which do not specify
+ * a fill and use a standard thickness line
+ *
+ * @param stroke Stroke color to be used (e.g. for the border/foreground)
+ * @param strokeAlpha Transparency to use for the stroke; 0 is transparent
+ * and 255 is fully opaque.
+ * @param lineStyle The SWT line style - such as {@link SWT#LINE_SOLID}.
+ */
+ private SwtDrawingStyle(RGB stroke, int strokeAlpha, int lineStyle) {
+ this(stroke, strokeAlpha, null, 255, 1, lineStyle);
+ }
+
+ /**
+ * Return the stroke/foreground/border RGB color description to be used for
+ * this style, or null if none
+ */
+ public RGB getStrokeColor() {
+ return mStroke;
+ }
+
+ /**
+ * Return the fill/background/interior RGB color description to be used for
+ * this style, or null if none
+ */
+ public RGB getFillColor() {
+ return mFill;
+ }
+
+ /** Return the line width to be used for this style */
+ public int getLineWidth() {
+ return mLineWidth;
+ }
+
+ /** Return the SWT line style to be used for this style */
+ public int getLineStyle() {
+ return mLineStyle;
+ }
+
+ /**
+ * Return the stroke alpha value (in the range 0,255) to be used for this
+ * style
+ */
+ public int getStrokeAlpha() {
+ return mStrokeAlpha;
+ }
+
+ /**
+ * Return the fill alpha value (in the range 0,255) to be used for this
+ * style
+ */
+ public int getFillAlpha() {
+ return mFillAlpha;
+ }
+
+ /**
+ * Return the corresponding SwtDrawingStyle for the given
+ * {@link DrawingStyle}
+ * @param style The style to convert from a {@link DrawingStyle} to a {@link SwtDrawingStyle}.
+ * @return A corresponding {@link SwtDrawingStyle}.
+ */
+ public static SwtDrawingStyle of(DrawingStyle style) {
+ switch (style) {
+ case SELECTION:
+ return SELECTION;
+ case GUIDELINE:
+ return GUIDELINE;
+ case GUIDELINE_SHADOW:
+ return GUIDELINE_SHADOW;
+ case GUIDELINE_DASHED:
+ return GUIDELINE_DASHED;
+ case DISTANCE:
+ return DISTANCE;
+ case GRID:
+ return GRID;
+ case HOVER:
+ return HOVER;
+ case HOVER_SELECTION:
+ return HOVER_SELECTION;
+ case ANCHOR:
+ return ANCHOR;
+ case OUTLINE:
+ return OUTLINE;
+ case DROP_ZONE:
+ return DROP_ZONE;
+ case DROP_ZONE_ACTIVE:
+ return DROP_ZONE_ACTIVE;
+ case DROP_RECIPIENT:
+ return DROP_RECIPIENT;
+ case DROP_PREVIEW:
+ return DROP_PREVIEW;
+ case RESIZE_PREVIEW:
+ return RESIZE_PREVIEW;
+ case RESIZE_FAIL:
+ return RESIZE_FAIL;
+ case HELP:
+ return HELP;
+ case INVALID:
+ return INVALID;
+ case DEPENDENCY:
+ return DEPENDENCY;
+ case CYCLE:
+ return CYCLE;
+ case DRAGGED:
+ return DRAGGED;
+ case EMPTY:
+ return EMPTY;
+ case CUSTOM1:
+ return CUSTOM1;
+ case CUSTOM2:
+ return CUSTOM2;
+
+ // Internal error
+ default:
+ throw new IllegalArgumentException("Unknown style " + style);
+ }
+ }
+
+ /** RGB description of the stroke/foreground/border color */
+ private final RGB mStroke;
+
+ /** RGB description of the fill/foreground/interior color */
+ private final RGB mFill;
+
+ /** Pixel thickness of the stroke/border */
+ private final int mLineWidth;
+
+ /** SWT line style of the border/stroke */
+ private final int mLineStyle;
+
+ /** Alpha (in the range 0-255) of the stroke/border */
+ private final int mStrokeAlpha;
+
+ /** Alpha (in the range 0-255) of the fill/interior */
+ private final int mFillAlpha;
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java
new file mode 100644
index 000000000..64e91bedf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtUtils.java
@@ -0,0 +1,457 @@
+/*
+ * Copyright (C) 2010 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 static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE;
+
+import com.android.ide.common.api.Rect;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontMetrics;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+
+import java.awt.Graphics;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferByte;
+import java.awt.image.DataBufferInt;
+import java.awt.image.WritableRaster;
+import java.util.List;
+
+/**
+ * Various generic SWT utilities such as image conversion.
+ */
+public class SwtUtils {
+
+ private SwtUtils() {
+ }
+
+ /**
+ * Returns the {@link PaletteData} describing the ARGB ordering expected from integers
+ * representing pixels for AWT {@link BufferedImage}.
+ *
+ * @param imageType the {@link BufferedImage#getType()} type
+ * @return A new {@link PaletteData} suitable for AWT images.
+ */
+ public static PaletteData getAwtPaletteData(int imageType) {
+ switch (imageType) {
+ case BufferedImage.TYPE_INT_RGB:
+ case BufferedImage.TYPE_INT_ARGB:
+ case BufferedImage.TYPE_INT_ARGB_PRE:
+ return new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF);
+
+ case BufferedImage.TYPE_3BYTE_BGR:
+ case BufferedImage.TYPE_4BYTE_ABGR:
+ case BufferedImage.TYPE_4BYTE_ABGR_PRE:
+ return new PaletteData(0x000000FF, 0x0000FF00, 0x00FF0000);
+
+ default:
+ throw new UnsupportedOperationException("RGB type not supported yet.");
+ }
+ }
+
+ /**
+ * Returns true if the given type of {@link BufferedImage} is supported for
+ * conversion. For unsupported formats, use
+ * {@link #convertToCompatibleFormat(BufferedImage)} first.
+ *
+ * @param imageType the {@link BufferedImage#getType()}
+ * @return true if we can convert the given buffered image format
+ */
+ private static boolean isSupportedPaletteType(int imageType) {
+ switch (imageType) {
+ case BufferedImage.TYPE_INT_RGB:
+ case BufferedImage.TYPE_INT_ARGB:
+ case BufferedImage.TYPE_INT_ARGB_PRE:
+ case BufferedImage.TYPE_3BYTE_BGR:
+ case BufferedImage.TYPE_4BYTE_ABGR:
+ case BufferedImage.TYPE_4BYTE_ABGR_PRE:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /** Converts the given arbitrary {@link BufferedImage} to another {@link BufferedImage}
+ * in a format that is supported (see {@link #isSupportedPaletteType(int)})
+ *
+ * @param image the image to be converted
+ * @return a new image that is in a guaranteed compatible format
+ */
+ private static BufferedImage convertToCompatibleFormat(BufferedImage image) {
+ BufferedImage converted = new BufferedImage(image.getWidth(), image.getHeight(),
+ BufferedImage.TYPE_INT_ARGB);
+ Graphics graphics = converted.getGraphics();
+ graphics.drawImage(image, 0, 0, null);
+ graphics.dispose();
+
+ return converted;
+ }
+
+ /**
+ * Converts an AWT {@link BufferedImage} into an equivalent SWT {@link Image}. Whether
+ * the transparency data is transferred is optional, and this method can also apply an
+ * alpha adjustment during the conversion.
+ * <p/>
+ * Implementation details: on Windows, the returned {@link Image} will have an ordering
+ * matching the Windows DIB (e.g. RGBA, not ARGB). Callers must make sure to use
+ * <code>Image.getImageData().paletteData</code> to get the right pixels out of the image.
+ *
+ * @param display The display where the SWT image will be shown
+ * @param awtImage The AWT {@link BufferedImage}
+ * @param transferAlpha If true, copy alpha data out of the source image
+ * @param globalAlpha If -1, do nothing, otherwise adjust the alpha of the final image
+ * by the given amount in the range [0,255]
+ * @return A new SWT {@link Image} with the same contents as the source
+ * {@link BufferedImage}
+ */
+ public static Image convertToSwt(Device display, BufferedImage awtImage,
+ boolean transferAlpha, int globalAlpha) {
+ if (!isSupportedPaletteType(awtImage.getType())) {
+ awtImage = convertToCompatibleFormat(awtImage);
+ }
+
+ int width = awtImage.getWidth();
+ int height = awtImage.getHeight();
+
+ WritableRaster raster = awtImage.getRaster();
+ DataBuffer dataBuffer = raster.getDataBuffer();
+ ImageData imageData =
+ new ImageData(width, height, 32, getAwtPaletteData(awtImage.getType()));
+
+ if (dataBuffer instanceof DataBufferInt) {
+ int[] imageDataBuffer = ((DataBufferInt) dataBuffer).getData();
+ imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
+ } else if (dataBuffer instanceof DataBufferByte) {
+ byte[] imageDataBuffer = ((DataBufferByte) dataBuffer).getData();
+ try {
+ imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
+ } catch (SWTException se) {
+ // Unsupported depth
+ return convertToSwt(display, convertToCompatibleFormat(awtImage),
+ transferAlpha, globalAlpha);
+ }
+ }
+
+ if (transferAlpha) {
+ byte[] alphaData = new byte[height * width];
+ for (int y = 0; y < height; y++) {
+ byte[] alphaRow = new byte[width];
+ for (int x = 0; x < width; x++) {
+ int alpha = awtImage.getRGB(x, y) >>> 24;
+
+ // We have to multiply in the alpha now since if we
+ // set ImageData.alpha, it will ignore the alphaData.
+ if (globalAlpha != -1) {
+ alpha = alpha * globalAlpha >> 8;
+ }
+
+ alphaRow[x] = (byte) alpha;
+ }
+ System.arraycopy(alphaRow, 0, alphaData, y * width, width);
+ }
+
+ imageData.alphaData = alphaData;
+ } else if (globalAlpha != -1) {
+ imageData.alpha = globalAlpha;
+ }
+
+ return new Image(display, imageData);
+ }
+
+ /**
+ * Converts a direct-color model SWT image to an equivalent AWT image. If the image
+ * does not have a supported color model, returns null. This method does <b>NOT</b>
+ * preserve alpha in the source image.
+ *
+ * @param swtImage the SWT image to be converted to AWT
+ * @return an AWT image representing the source SWT image
+ */
+ public static BufferedImage convertToAwt(Image swtImage) {
+ ImageData swtData = swtImage.getImageData();
+ BufferedImage awtImage =
+ new BufferedImage(swtData.width, swtData.height, BufferedImage.TYPE_INT_ARGB);
+ PaletteData swtPalette = swtData.palette;
+ if (swtPalette.isDirect) {
+ PaletteData awtPalette = getAwtPaletteData(awtImage.getType());
+
+ if (swtPalette.equals(awtPalette)) {
+ // No color conversion needed.
+ for (int y = 0; y < swtData.height; y++) {
+ for (int x = 0; x < swtData.width; x++) {
+ int pixel = swtData.getPixel(x, y);
+ awtImage.setRGB(x, y, 0xFF000000 | pixel);
+ }
+ }
+ } else {
+ // We need to remap the colors
+ int sr = -awtPalette.redShift + swtPalette.redShift;
+ int sg = -awtPalette.greenShift + swtPalette.greenShift;
+ int sb = -awtPalette.blueShift + swtPalette.blueShift;
+
+ for (int y = 0; y < swtData.height; y++) {
+ for (int x = 0; x < swtData.width; x++) {
+ int pixel = swtData.getPixel(x, y);
+
+ int r = pixel & swtPalette.redMask;
+ int g = pixel & swtPalette.greenMask;
+ int b = pixel & swtPalette.blueMask;
+ r = (sr < 0) ? r >>> -sr : r << sr;
+ g = (sg < 0) ? g >>> -sg : g << sg;
+ b = (sb < 0) ? b >>> -sb : b << sb;
+
+ pixel = 0xFF000000 | r | g | b;
+ awtImage.setRGB(x, y, pixel);
+ }
+ }
+ }
+ } else {
+ return null;
+ }
+
+ return awtImage;
+ }
+
+ /**
+ * Creates a new image from a source image where the contents from a given set of
+ * bounding boxes are copied into the new image and the rest is left transparent. A
+ * scale can be applied to make the resulting image larger or smaller than the source
+ * image. Note that the alpha channel in the original image is ignored, and the alpha
+ * values for the painted rectangles will be set to a specific value passed into this
+ * function.
+ *
+ * @param image the source image
+ * @param rectangles the set of rectangles (bounding boxes) to copy from the source
+ * image
+ * @param boundingBox the bounding rectangle of the rectangle list, which can be
+ * computed by {@link ImageUtils#getBoundingRectangle}
+ * @param scale a scale factor to apply to the result, e.g. 0.5 to shrink the
+ * destination down 50%, 1.0 to leave it alone and 2.0 to zoom in by
+ * doubling the image size
+ * @param alpha the alpha (in the range 0-255) that painted bits should be set to
+ * @return a pair of the rendered cropped image, and the location within the source
+ * image that the crop begins (multiplied by the scale). May return null if
+ * there are no selected items.
+ */
+ public static Image drawRectangles(Image image,
+ List<Rectangle> rectangles, Rectangle boundingBox, double scale, byte alpha) {
+
+ if (rectangles.size() == 0 || boundingBox == null || boundingBox.isEmpty()) {
+ return null;
+ }
+
+ ImageData srcData = image.getImageData();
+ int destWidth = (int) (scale * boundingBox.width);
+ int destHeight = (int) (scale * boundingBox.height);
+
+ ImageData destData = new ImageData(destWidth, destHeight, srcData.depth, srcData.palette);
+ byte[] alphaData = new byte[destHeight * destWidth];
+ destData.alphaData = alphaData;
+
+ for (Rectangle bounds : rectangles) {
+ int dx1 = bounds.x - boundingBox.x;
+ int dy1 = bounds.y - boundingBox.y;
+ int dx2 = dx1 + bounds.width;
+ int dy2 = dy1 + bounds.height;
+
+ dx1 *= scale;
+ dy1 *= scale;
+ dx2 *= scale;
+ dy2 *= scale;
+
+ int sx1 = bounds.x;
+ int sy1 = bounds.y;
+ int sx2 = sx1 + bounds.width;
+ int sy2 = sy1 + bounds.height;
+
+ if (scale == 1.0d) {
+ for (int dy = dy1, sy = sy1; dy < dy2; dy++, sy++) {
+ for (int dx = dx1, sx = sx1; dx < dx2; dx++, sx++) {
+ destData.setPixel(dx, dy, srcData.getPixel(sx, sy));
+ alphaData[dy * destWidth + dx] = alpha;
+ }
+ }
+ } else {
+ // Scaled copy.
+ int sxDelta = sx2 - sx1;
+ int dxDelta = dx2 - dx1;
+ int syDelta = sy2 - sy1;
+ int dyDelta = dy2 - dy1;
+ for (int dy = dy1, sy = sy1; dy < dy2; dy++, sy = (dy - dy1) * syDelta / dyDelta
+ + sy1) {
+ for (int dx = dx1, sx = sx1; dx < dx2; dx++, sx = (dx - dx1) * sxDelta
+ / dxDelta + sx1) {
+ assert sx < sx2 && sy < sy2;
+ destData.setPixel(dx, dy, srcData.getPixel(sx, sy));
+ alphaData[dy * destWidth + dx] = alpha;
+ }
+ }
+ }
+ }
+
+ return new Image(image.getDevice(), destData);
+ }
+
+ /**
+ * Creates a new empty/blank image of the given size
+ *
+ * @param display the display to associate the image with
+ * @param width the width of the image
+ * @param height the height of the image
+ * @return a new blank image of the given size
+ */
+ public static Image createEmptyImage(Display display, int width, int height) {
+ BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ return SwtUtils.convertToSwt(display, image, false, 0);
+ }
+
+ /**
+ * Converts the given SWT {@link Rectangle} into an ADT {@link Rect}
+ *
+ * @param swtRect the SWT {@link Rectangle}
+ * @return an equivalent {@link Rect}
+ */
+ public static Rect toRect(Rectangle swtRect) {
+ return new Rect(swtRect.x, swtRect.y, swtRect.width, swtRect.height);
+ }
+
+ /**
+ * Sets the values of the given ADT {@link Rect} to the values of the given SWT
+ * {@link Rectangle}
+ *
+ * @param target the ADT {@link Rect} to modify
+ * @param source the SWT {@link Rectangle} to read values from
+ */
+ public static void set(Rect target, Rectangle source) {
+ target.set(source.x, source.y, source.width, source.height);
+ }
+
+ /**
+ * Compares an ADT {@link Rect} with an SWT {@link Rectangle} and returns true if they
+ * are equivalent
+ *
+ * @param r1 the ADT {@link Rect}
+ * @param r2 the SWT {@link Rectangle}
+ * @return true if the two rectangles are equivalent
+ */
+ public static boolean equals(Rect r1, Rectangle r2) {
+ return r1.x == r2.x && r1.y == r2.y && r1.w == r2.width && r1.h == r2.height;
+
+ }
+
+ /**
+ * Get the average width of the font used by the given control
+ *
+ * @param display the display associated with the font usage
+ * @param font the font to look up the average character width for
+ * @return the average width, in pixels, of the given font
+ */
+ public static final int getAverageCharWidth(Display display, Font font) {
+ GC gc = new GC(display);
+ gc.setFont(font);
+ FontMetrics fontMetrics = gc.getFontMetrics();
+ int width = fontMetrics.getAverageCharWidth();
+ gc.dispose();
+ return width;
+ }
+
+ /**
+ * Get the average width of the given font
+ *
+ * @param control the control to look up the default font for
+ * @return the average width, in pixels, of the current font in the control
+ */
+ public static final int getAverageCharWidth(Control control) {
+ GC gc = new GC(control.getDisplay());
+ int width = gc.getFontMetrics().getAverageCharWidth();
+ gc.dispose();
+ return width;
+ }
+
+ /**
+ * Draws a drop shadow for the given rectangle into the given context. It
+ * will not draw anything if the rectangle is smaller than a minimum
+ * determined by the assets used to draw the shadow graphics.
+ * <p>
+ * This corresponds to {@link ImageUtils#drawRectangleShadow(Graphics, int, int, int, int)},
+ * but applied directly to an SWT graphics context instead, such that no image conversion
+ * has to be performed.
+ * <p>
+ * Make sure to keep changes in the visual appearance here in sync with the
+ * AWT version in {@link ImageUtils#drawRectangleShadow(Graphics, int, int, int, int)}.
+ *
+ * @param gc the graphics context to draw into
+ * @param x the left coordinate of the left hand side of the rectangle
+ * @param y the top coordinate of the top of the rectangle
+ * @param width the width of the rectangle
+ * @param height the height of the rectangle
+ */
+ public static final void drawRectangleShadow(GC gc, int x, int y, int width, int height) {
+ if (sShadowBottomLeft == null) {
+ IconFactory icons = IconFactory.getInstance();
+ // See ImageUtils.drawRectangleShadow for an explanation of the assets.
+ sShadowBottomLeft = icons.getIcon("shadow-bl"); //$NON-NLS-1$
+ sShadowBottom = icons.getIcon("shadow-b"); //$NON-NLS-1$
+ sShadowBottomRight = icons.getIcon("shadow-br"); //$NON-NLS-1$
+ sShadowRight = icons.getIcon("shadow-r"); //$NON-NLS-1$
+ sShadowTopRight = icons.getIcon("shadow-tr"); //$NON-NLS-1$
+ assert sShadowBottomRight.getImageData().width == SHADOW_SIZE;
+ assert sShadowBottomRight.getImageData().height == SHADOW_SIZE;
+ }
+
+ ImageData bottomLeftData = sShadowBottomLeft.getImageData();
+ ImageData topRightData = sShadowTopRight.getImageData();
+ ImageData bottomData = sShadowBottom.getImageData();
+ ImageData rightData = sShadowRight.getImageData();
+ int blWidth = bottomLeftData.width;
+ int trHeight = topRightData.height;
+ if (width < blWidth) {
+ return;
+ }
+ if (height < trHeight) {
+ return;
+ }
+
+ gc.drawImage(sShadowBottomLeft, x, y + height);
+ gc.drawImage(sShadowBottomRight, x + width, y + height);
+ gc.drawImage(sShadowTopRight, x + width, y);
+ gc.drawImage(sShadowBottom,
+ 0, 0,
+ bottomData.width, bottomData.height,
+ x + bottomLeftData.width, y + height,
+ width - bottomLeftData.width, bottomData.height);
+ gc.drawImage(sShadowRight,
+ 0, 0,
+ rightData.width, rightData.height,
+ x + width, y + topRightData.height,
+ rightData.width, height - topRightData.height);
+ }
+
+ private static Image sShadowBottomLeft;
+ private static Image sShadowBottom;
+ private static Image sShadowBottomRight;
+ private static Image sShadowRight;
+ private static Image sShadowTopRight;
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
new file mode 100644
index 000000000..d247e28d7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
@@ -0,0 +1,771 @@
+/*
+ * Copyright (C) 2010 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 static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.VIEW_MERGE;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+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.utils.Pair;
+
+import org.eclipse.swt.graphics.Rectangle;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.RandomAccess;
+import java.util.Set;
+
+/**
+ * The view hierarchy class manages a set of view info objects and performs find
+ * operations on this set.
+ */
+public class ViewHierarchy {
+ private static final boolean DUMP_INFO = false;
+
+ private LayoutCanvas mCanvas;
+
+ /**
+ * Constructs a new {@link ViewHierarchy} tied to the given
+ * {@link LayoutCanvas}.
+ *
+ * @param canvas The {@link LayoutCanvas} to create a {@link ViewHierarchy}
+ * for.
+ */
+ public ViewHierarchy(LayoutCanvas canvas) {
+ mCanvas = canvas;
+ }
+
+ /**
+ * The CanvasViewInfo root created by the last call to {@link #setSession}
+ * with a valid layout.
+ * <p/>
+ * This <em>can</em> be null to indicate we're dealing with an empty document with
+ * no root node. Null here does not mean the result was invalid, merely that the XML
+ * had no content to display -- we need to treat an empty document as valid so that
+ * we can drop new items in it.
+ */
+ private CanvasViewInfo mLastValidViewInfoRoot;
+
+ /**
+ * True when the last {@link #setSession} provided a valid {@link LayoutScene}.
+ * <p/>
+ * When false this means the canvas is displaying an out-dated result image & bounds and some
+ * features should be disabled accordingly such a drag'n'drop.
+ * <p/>
+ * Note that an empty document (with a null {@link #mLastValidViewInfoRoot}) is considered
+ * valid since it is an acceptable drop target.
+ */
+ private boolean mIsResultValid;
+
+ /**
+ * A list of invisible parents (see {@link CanvasViewInfo#isInvisible()} for
+ * details) in the current view hierarchy.
+ */
+ private final List<CanvasViewInfo> mInvisibleParents = new ArrayList<CanvasViewInfo>();
+
+ /**
+ * A read-only view of {@link #mInvisibleParents}; note that this is NOT a copy so it
+ * reflects updates to the underlying {@link #mInvisibleParents} list.
+ */
+ private final List<CanvasViewInfo> mInvisibleParentsReadOnly =
+ Collections.unmodifiableList(mInvisibleParents);
+
+ /**
+ * Flag which records whether or not we have any exploded parent nodes in this
+ * view hierarchy. This is used to track whether or not we need to recompute the
+ * layout when we exit show-all-invisible-parents mode (see
+ * {@link LayoutCanvas#showInvisibleViews}).
+ */
+ private boolean mExplodedParents;
+
+ /**
+ * Bounds of included views in the current view hierarchy when rendered in other context
+ */
+ private List<Rectangle> mIncludedBounds;
+
+ /** The render session for the current view hierarchy */
+ private RenderSession mSession;
+
+ /** Map from nodes to canvas view infos */
+ private Map<UiViewElementNode, CanvasViewInfo> mNodeToView = Collections.emptyMap();
+
+ /** Map from DOM nodes to canvas view infos */
+ private Map<Node, CanvasViewInfo> mDomNodeToView = Collections.emptyMap();
+
+ /**
+ * Disposes the view hierarchy content.
+ */
+ public void dispose() {
+ if (mSession != null) {
+ mSession.dispose();
+ mSession = null;
+ }
+ }
+
+
+ /**
+ * 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 session, 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 LayoutCanvas#showInvisibleViews}) where individual invisible
+ * nodes are padded during certain interactions.
+ */
+ /* package */ void setSession(RenderSession session, Set<UiElementNode> explodedNodes,
+ boolean layoutlib5) {
+ // replace the previous scene, so the previous scene must be disposed.
+ if (mSession != null) {
+ mSession.dispose();
+ }
+
+ mSession = session;
+ mIsResultValid = (session != null && session.getResult().isSuccess());
+ mExplodedParents = false;
+ mNodeToView = new HashMap<UiViewElementNode, CanvasViewInfo>(50);
+ if (mIsResultValid && session != null) {
+ List<ViewInfo> rootList = session.getRootViews();
+
+ Pair<CanvasViewInfo,List<Rectangle>> infos = null;
+
+ if (rootList == null || rootList.size() == 0) {
+ // Special case: Look to see if this is really an empty <merge> view,
+ // which shows up without any ViewInfos in the merge. In that case we
+ // want to manufacture an empty view, such that we can target the view
+ // via drag & drop, etc.
+ if (hasMergeRoot()) {
+ ViewInfo mergeRoot = createMergeInfo(session);
+ infos = CanvasViewInfo.create(mergeRoot, layoutlib5);
+ } else {
+ infos = null;
+ }
+ } else {
+ if (rootList.size() > 1 && hasMergeRoot()) {
+ ViewInfo mergeRoot = createMergeInfo(session);
+ mergeRoot.setChildren(rootList);
+ infos = CanvasViewInfo.create(mergeRoot, layoutlib5);
+ } else {
+ ViewInfo root = rootList.get(0);
+
+ if (root != null) {
+ infos = CanvasViewInfo.create(root, layoutlib5);
+ if (DUMP_INFO) {
+ dump(session, root, 0);
+ }
+ } else {
+ infos = null;
+ }
+ }
+ }
+ if (infos != null) {
+ mLastValidViewInfoRoot = infos.getFirst();
+ mIncludedBounds = infos.getSecond();
+
+ if (mLastValidViewInfoRoot.getUiViewNode() == null &&
+ mLastValidViewInfoRoot.getChildren().isEmpty()) {
+ GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor();
+ if (editor.getIncludedWithin() != null) {
+ // Somehow, this view was supposed to be rendered within another
+ // view, yet this view was rendered as part of the other view.
+ // In that case, abort attempting to show included in; clear the
+ // include context and trigger a standalone re-render.
+ editor.showIn(null);
+ return;
+ }
+ }
+
+ } else {
+ mLastValidViewInfoRoot = null;
+ mIncludedBounds = null;
+ }
+
+ updateNodeProxies(mLastValidViewInfoRoot);
+
+ // Update the data structures related to tracking invisible and exploded nodes.
+ // We need to find the {@link CanvasViewInfo} objects that correspond to
+ // the passed in {@link UiElementNode} keys that were re-rendered, and mark
+ // them as exploded and store them in a list for rendering.
+ mExplodedParents = false;
+ mInvisibleParents.clear();
+ addInvisibleParents(mLastValidViewInfoRoot, explodedNodes);
+
+ mDomNodeToView = new HashMap<Node, CanvasViewInfo>(mNodeToView.size());
+ for (Map.Entry<UiViewElementNode, CanvasViewInfo> entry : mNodeToView.entrySet()) {
+ mDomNodeToView.put(entry.getKey().getXmlNode(), entry.getValue());
+ }
+
+ // Update the selection
+ mCanvas.getSelectionManager().sync();
+ } else {
+ mIncludedBounds = null;
+ mInvisibleParents.clear();
+ mDomNodeToView = Collections.emptyMap();
+ }
+ }
+
+ private ViewInfo createMergeInfo(RenderSession session) {
+ BufferedImage image = session.getImage();
+ ControlPoint imageSize = ControlPoint.create(mCanvas,
+ mCanvas.getHorizontalTransform().getMargin() + image.getWidth(),
+ mCanvas.getVerticalTransform().getMargin() + image.getHeight());
+ LayoutPoint layoutSize = imageSize.toLayout();
+ UiDocumentNode model = mCanvas.getEditorDelegate().getUiRootNode();
+ List<UiElementNode> children = model.getUiChildren();
+ return new ViewInfo(VIEW_MERGE, children.get(0), 0, 0, layoutSize.x, layoutSize.y);
+ }
+
+ /**
+ * Returns true if this view hierarchy corresponds to an editor that has a {@code
+ * <merge>} tag at the root
+ *
+ * @return true if there is a {@code <merge>} at the root of this editor's document
+ */
+ private boolean hasMergeRoot() {
+ UiDocumentNode model = mCanvas.getEditorDelegate().getUiRootNode();
+ if (model != null) {
+ List<UiElementNode> children = model.getUiChildren();
+ if (children != null && children.size() > 0
+ && VIEW_MERGE.equals(children.get(0).getDescriptor().getXmlName())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Creates or updates the node proxy for this canvas view info.
+ * <p/>
+ * Since proxies are reused, this will update the bounds of an existing proxy when the
+ * canvas is refreshed and a view changes position or size.
+ * <p/>
+ * This is a recursive call that updates the whole hierarchy starting at the given
+ * view info.
+ */
+ private void updateNodeProxies(CanvasViewInfo vi) {
+ if (vi == null) {
+ return;
+ }
+
+ UiViewElementNode key = vi.getUiViewNode();
+
+ if (key != null) {
+ mCanvas.getNodeFactory().create(vi);
+ mNodeToView.put(key, vi);
+ }
+
+ for (CanvasViewInfo child : vi.getChildren()) {
+ updateNodeProxies(child);
+ }
+ }
+
+ /**
+ * Make a pass over the view hierarchy and look for two things:
+ * <ol>
+ * <li>Invisible parents. These are nodes that can hold children and have empty
+ * bounds. These are then added to the {@link #mInvisibleParents} list.
+ * <li>Exploded nodes. These are nodes that were previously marked as invisible, and
+ * subsequently rendered by a recomputed layout. They now no longer have empty bounds,
+ * but should be specially marked via {@link CanvasViewInfo#setExploded} such that we
+ * for example in selection operations can determine if we need to recompute the
+ * layout.
+ * </ol>
+ *
+ * @param vi
+ * @param invisibleNodes
+ */
+ private void addInvisibleParents(CanvasViewInfo vi, Set<UiElementNode> invisibleNodes) {
+ if (vi == null) {
+ return;
+ }
+
+ if (vi.isInvisible()) {
+ mInvisibleParents.add(vi);
+ } else if (invisibleNodes != null) {
+ UiViewElementNode key = vi.getUiViewNode();
+
+ if (key != null && invisibleNodes.contains(key)) {
+ vi.setExploded(true);
+ mExplodedParents = true;
+ mInvisibleParents.add(vi);
+ }
+ }
+
+ for (CanvasViewInfo child : vi.getChildren()) {
+ addInvisibleParents(child, invisibleNodes);
+ }
+ }
+
+ /**
+ * Returns the current {@link RenderSession}.
+ * @return the session or null if none have been set.
+ */
+ public RenderSession getSession() {
+ return mSession;
+ }
+
+ /**
+ * Returns true when the last {@link #setSession} provided a valid
+ * {@link RenderSession}.
+ * <p/>
+ * When false this means the canvas is displaying an out-dated result image & bounds and some
+ * features should be disabled accordingly such a drag'n'drop.
+ * <p/>
+ * Note that an empty document (with a null {@link #getRoot()}) is considered
+ * valid since it is an acceptable drop target.
+ * @return True when this {@link ViewHierarchy} contains a valid hierarchy of views.
+ */
+ public boolean isValid() {
+ return mIsResultValid;
+ }
+
+ /**
+ * Returns true if the last valid content of the canvas represents an empty document.
+ * @return True if the last valid content of the canvas represents an empty document.
+ */
+ public boolean isEmpty() {
+ return mLastValidViewInfoRoot == null;
+ }
+
+ /**
+ * Returns true if we have parents in this hierarchy that are invisible (e.g. because
+ * they have no children and zero layout bounds).
+ *
+ * @return True if we have invisible parents.
+ */
+ public boolean hasInvisibleParents() {
+ return mInvisibleParents.size() > 0;
+ }
+
+ /**
+ * Returns true if we have views that were exploded during rendering
+ * @return True if we have exploded parents
+ */
+ public boolean hasExplodedParents() {
+ return mExplodedParents;
+ }
+
+ /** Locates and return any views that overlap the given selection rectangle.
+ * @param topLeft The top left corner of the selection rectangle.
+ * @param bottomRight The bottom right corner of the selection rectangle.
+ * @return A collection of {@link CanvasViewInfo} objects that overlap the
+ * rectangle.
+ */
+ public Collection<CanvasViewInfo> findWithin(
+ LayoutPoint topLeft,
+ LayoutPoint bottomRight) {
+ Rectangle selectionRectangle = new Rectangle(topLeft.x, topLeft.y, bottomRight.x
+ - topLeft.x, bottomRight.y - topLeft.y);
+ List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
+ addWithin(mLastValidViewInfoRoot, selectionRectangle, infos);
+ return infos;
+ }
+
+ /**
+ * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
+ * <p/>
+ * Tries to find the inner most child matching the given x,y coordinates in the view
+ * info sub-tree. This uses the potentially-expanded selection bounds.
+ *
+ * Returns null if not found.
+ */
+ private void addWithin(
+ CanvasViewInfo canvasViewInfo,
+ Rectangle canvasRectangle,
+ List<CanvasViewInfo> infos) {
+ if (canvasViewInfo == null) {
+ return;
+ }
+ Rectangle r = canvasViewInfo.getSelectionRect();
+ if (canvasRectangle.intersects(r)) {
+
+ // try to find a matching child first
+ for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+ addWithin(child, canvasRectangle, infos);
+ }
+
+ if (canvasViewInfo != mLastValidViewInfoRoot) {
+ infos.add(canvasViewInfo);
+ }
+ }
+ }
+
+ /**
+ * Locates and returns the {@link CanvasViewInfo} corresponding to the given
+ * node, or null if it cannot be found.
+ *
+ * @param node The node we want to find a corresponding
+ * {@link CanvasViewInfo} for.
+ * @return The {@link CanvasViewInfo} corresponding to the given node, or
+ * null if no match was found.
+ */
+ @Nullable
+ public CanvasViewInfo findViewInfoFor(@Nullable Node node) {
+ CanvasViewInfo vi = mDomNodeToView.get(node);
+
+ if (vi == null) {
+ if (node == null) {
+ return null;
+ } else if (node.getNodeType() == Node.TEXT_NODE) {
+ return mDomNodeToView.get(node.getParentNode());
+ } else if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
+ return mDomNodeToView.get(((Attr) node).getOwnerElement());
+ } else if (node.getNodeType() == Node.DOCUMENT_NODE) {
+ return mDomNodeToView.get(((Document) node).getDocumentElement());
+ }
+ }
+
+ return vi;
+ }
+
+ /**
+ * Tries to find the inner most child matching the given x,y coordinates in
+ * the view info sub-tree, starting at the last know view info root. This
+ * uses the potentially-expanded selection bounds.
+ * <p/>
+ * Returns null if not found or if there's no view info root.
+ *
+ * @param p The point at which to look for the deepest match in the view
+ * hierarchy
+ * @return A {@link CanvasViewInfo} that intersects the given point, or null
+ * if nothing was found.
+ */
+ public CanvasViewInfo findViewInfoAt(LayoutPoint p) {
+ if (mLastValidViewInfoRoot == null) {
+ return null;
+ }
+
+ return findViewInfoAt_Recursive(p, mLastValidViewInfoRoot);
+ }
+
+ /**
+ * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
+ * <p/>
+ * Tries to find the inner most child matching the given x,y coordinates in the view
+ * info sub-tree. This uses the potentially-expanded selection bounds.
+ *
+ * Returns null if not found.
+ */
+ private CanvasViewInfo findViewInfoAt_Recursive(LayoutPoint p, CanvasViewInfo canvasViewInfo) {
+ if (canvasViewInfo == null) {
+ return null;
+ }
+ Rectangle r = canvasViewInfo.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+
+ // try to find a matching child first
+ // Iterate in REVERSE z order such that siblings on top
+ // are checked before earlier siblings (this matters in layouts like
+ // FrameLayout and in <merge> contexts where the views are sitting on top
+ // of each other and we want to select the same view as the one drawn
+ // on top of the others
+ List<CanvasViewInfo> children = canvasViewInfo.getChildren();
+ assert children instanceof RandomAccess;
+ for (int i = children.size() - 1; i >= 0; i--) {
+ CanvasViewInfo child = children.get(i);
+ CanvasViewInfo v = findViewInfoAt_Recursive(p, child);
+ if (v != null) {
+ return v;
+ }
+ }
+
+ // if no children matched, this is the view that we're looking for
+ return canvasViewInfo;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a list of all the possible alternatives for a given view at the given
+ * position. This is used to build and manage the "alternate" selection that cycles
+ * around the parents or children of the currently selected element.
+ */
+ /* package */ List<CanvasViewInfo> findAltViewInfoAt(LayoutPoint p) {
+ if (mLastValidViewInfoRoot != null) {
+ return findAltViewInfoAt_Recursive(p, mLastValidViewInfoRoot, null);
+ }
+
+ return null;
+ }
+
+ /**
+ * Internal recursive version of {@link #findAltViewInfoAt(int, int, CanvasViewInfo)}.
+ * Please don't use directly.
+ */
+ private List<CanvasViewInfo> findAltViewInfoAt_Recursive(
+ LayoutPoint p, CanvasViewInfo parent, List<CanvasViewInfo> outList) {
+ Rectangle r;
+
+ if (outList == null) {
+ outList = new ArrayList<CanvasViewInfo>();
+
+ if (parent != null) {
+ // add the parent root only once
+ r = parent.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+ outList.add(parent);
+ }
+ }
+ }
+
+ if (parent != null && !parent.getChildren().isEmpty()) {
+ // then add all children that match the position
+ for (CanvasViewInfo child : parent.getChildren()) {
+ r = child.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+ outList.add(child);
+ }
+ }
+
+ // finally recurse in the children
+ for (CanvasViewInfo child : parent.getChildren()) {
+ r = child.getSelectionRect();
+ if (r.contains(p.x, p.y)) {
+ findAltViewInfoAt_Recursive(p, child, outList);
+ }
+ }
+ }
+
+ return outList;
+ }
+
+ /**
+ * Locates and returns the {@link CanvasViewInfo} corresponding to the given
+ * node, or null if it cannot be found.
+ *
+ * @param node The node we want to find a corresponding
+ * {@link CanvasViewInfo} for.
+ * @return The {@link CanvasViewInfo} corresponding to the given node, or
+ * null if no match was found.
+ */
+ public CanvasViewInfo findViewInfoFor(INode node) {
+ return findViewInfoFor((NodeProxy) node);
+ }
+
+ /**
+ * Tries to find a child with the same view key in the view info sub-tree.
+ * Returns null if not found.
+ *
+ * @param viewKey The view key that a matching {@link CanvasViewInfo} should
+ * have as its key.
+ * @return A {@link CanvasViewInfo} matching the given key, or null if not
+ * found.
+ */
+ public CanvasViewInfo findViewInfoFor(UiElementNode viewKey) {
+ return mNodeToView.get(viewKey);
+ }
+
+ /**
+ * Tries to find a child with the given node proxy as the view key.
+ * Returns null if not found.
+ *
+ * @param proxy The view key that a matching {@link CanvasViewInfo} should
+ * have as its key.
+ * @return A {@link CanvasViewInfo} matching the given key, or null if not
+ * found.
+ */
+ @Nullable
+ public CanvasViewInfo findViewInfoFor(@Nullable NodeProxy proxy) {
+ if (proxy == null) {
+ return null;
+ }
+ return mNodeToView.get(proxy.getNode());
+ }
+
+ /**
+ * Returns a list of ALL ViewInfos (possibly excluding the root, depending
+ * on the parameter for that).
+ *
+ * @param includeRoot If true, include the root in the list, otherwise
+ * exclude it (but include all its children)
+ * @return A list of canvas view infos.
+ */
+ public List<CanvasViewInfo> findAllViewInfos(boolean includeRoot) {
+ List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
+ if (mIsResultValid && mLastValidViewInfoRoot != null) {
+ findAllViewInfos(infos, mLastValidViewInfoRoot, includeRoot);
+ }
+
+ return infos;
+ }
+
+ private void findAllViewInfos(List<CanvasViewInfo> result, CanvasViewInfo canvasViewInfo,
+ boolean includeRoot) {
+ if (canvasViewInfo != null) {
+ if (includeRoot || !canvasViewInfo.isRoot()) {
+ result.add(canvasViewInfo);
+ }
+ for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+ findAllViewInfos(result, child, true);
+ }
+ }
+ }
+
+ /**
+ * Returns the root of the view hierarchy, if any (could be null, for example
+ * on rendering failure).
+ *
+ * @return The current view hierarchy, or null
+ */
+ public CanvasViewInfo getRoot() {
+ return mLastValidViewInfoRoot;
+ }
+
+ /**
+ * Returns a collection of views that have zero bounds and that correspond to empty
+ * parents. Note that the views may not actually have zero bounds; in particular, if
+ * they are exploded ({@link CanvasViewInfo#isExploded()}, then they will have the
+ * bounds of a shown invisible node. Therefore, this method returns the views that
+ * would be invisible in a real rendering of the scene.
+ *
+ * @return A collection of empty parent views.
+ */
+ public List<CanvasViewInfo> getInvisibleViews() {
+ return mInvisibleParentsReadOnly;
+ }
+
+ /**
+ * Returns the invisible nodes (the {@link UiElementNode} objects corresponding
+ * to the {@link CanvasViewInfo} objects returned from {@link #getInvisibleViews()}.
+ * We are pulling out the nodes since they preserve their identity across layout
+ * rendering, and in particular we return it as a set such that the layout renderer
+ * can perform quick identity checks when looking up attribute values during the
+ * rendering process.
+ *
+ * @return A set of the invisible nodes.
+ */
+ public Set<UiElementNode> getInvisibleNodes() {
+ if (mInvisibleParents.size() == 0) {
+ return Collections.emptySet();
+ }
+
+ Set<UiElementNode> nodes = new HashSet<UiElementNode>(mInvisibleParents.size());
+ for (CanvasViewInfo info : mInvisibleParents) {
+ UiViewElementNode node = info.getUiViewNode();
+ if (node != null) {
+ nodes.add(node);
+ }
+ }
+
+ return nodes;
+ }
+
+ /**
+ * Returns the list of bounds for included views in the current view hierarchy. Can be null
+ * when there are no included views.
+ *
+ * @return a list of included view bounds, or null
+ */
+ public List<Rectangle> getIncludedBounds() {
+ return mIncludedBounds;
+ }
+
+ /**
+ * Returns a map of the default properties for the given view object in this session
+ *
+ * @param viewObject the object to look up the properties map for
+ * @return the map of properties, or null if not found
+ */
+ @Nullable
+ public Map<String, String> getDefaultProperties(@NonNull Object viewObject) {
+ if (mSession != null) {
+ return mSession.getDefaultProperties(viewObject);
+ }
+
+ return null;
+ }
+
+ /**
+ * Dumps a {@link ViewInfo} hierarchy to stdout
+ *
+ * @param session the corresponding session, if any
+ * @param info the {@link ViewInfo} object to dump
+ * @param depth the depth to indent it to
+ */
+ public static void dump(RenderSession session, ViewInfo info, int depth) {
+ if (DUMP_INFO) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < depth; i++) {
+ sb.append(" "); //$NON-NLS-1$
+ }
+ sb.append(info.getClassName());
+ sb.append(" ["); //$NON-NLS-1$
+ sb.append(info.getLeft());
+ sb.append(","); //$NON-NLS-1$
+ sb.append(info.getTop());
+ sb.append(","); //$NON-NLS-1$
+ sb.append(info.getRight());
+ sb.append(","); //$NON-NLS-1$
+ sb.append(info.getBottom());
+ sb.append("]"); //$NON-NLS-1$
+ Object cookie = info.getCookie();
+ if (cookie instanceof UiViewElementNode) {
+ sb.append(" "); //$NON-NLS-1$
+ UiViewElementNode node = (UiViewElementNode) cookie;
+ sb.append("<"); //$NON-NLS-1$
+ sb.append(node.getDescriptor().getXmlName());
+ sb.append(">"); //$NON-NLS-1$
+
+ String id = node.getAttributeValue(ATTR_ID);
+ if (id != null && !id.isEmpty()) {
+ sb.append(" ");
+ sb.append(id);
+ }
+ } else if (cookie != null) {
+ sb.append(" " + cookie); //$NON-NLS-1$
+ }
+ /* Display defaults?
+ if (info.getViewObject() != null) {
+ Map<String, String> defaults = session.getDefaultProperties(info.getCookie());
+ sb.append(" - defaults: "); //$NON-NLS-1$
+ sb.append(defaults);
+ sb.append('\n');
+ }
+ */
+
+ System.out.println(sb.toString());
+
+ for (ViewInfo child : info.getChildren()) {
+ dump(session, child, depth + 1);
+ }
+ }
+ }
+}