diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src')
34 files changed, 3906 insertions, 1822 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/Pair.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/Pair.java index 15c3ba9..950f6b9 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/Pair.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/Pair.java @@ -23,6 +23,9 @@ package com.android.ide.common.layout; * classes using this Pair by a more dedicated data structure (so we don't have * to pass around generic signatures as is currently done, though at least the * construction is helped a bit by the {@link #of} factory method. + * + * @param <S> The type of the first value + * @param <T> The type of the second value */ class Pair<S,T> { private final S mFirst; @@ -85,4 +88,4 @@ class Pair<S,T> { return false; return true; } -}
\ No newline at end of file +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java index c78a523..7ac8354 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java @@ -609,7 +609,8 @@ public class RelativeLayoutRule extends BaseLayout { } for (String it : data.getCurr().getAttr()) { - newChild.setAttribute(ANDROID_URI, "layout_" + it, id != null ? id : "true"); + newChild.setAttribute(ANDROID_URI, + "layout_" + it, id != null ? id : "true"); } addInnerElements(newChild, element, idMap); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java index 117138e..1e745b3 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java @@ -37,6 +37,7 @@ import org.eclipse.core.runtime.QualifiedName; import org.eclipse.core.runtime.Status; import org.eclipse.jface.action.IAction; import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.swt.widgets.Display; @@ -66,6 +67,7 @@ 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.ui.StructuredTextEditor; +import org.eclipse.wst.xml.core.internal.document.NodeContainer; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; import org.w3c.dom.Document; import org.w3c.dom.Node; @@ -81,6 +83,7 @@ import java.net.URL; * Derived classes must implement createFormPages to create the forms before the * source editor. This can be a no-op if desired. */ +@SuppressWarnings("restriction") // Uses XML model, which has no non-restricted replacement yet public abstract class AndroidXmlEditor extends FormEditor implements IResourceChangeListener { /** Preference name for the current page of this file */ @@ -940,6 +943,38 @@ public abstract class AndroidXmlEditor extends FormEditor implements IResourceCh } /** + * Get the XML text directly from the editor. + * + * @param xmlNode The node whose XML text we want to obtain. + * @return The XML representation of the {@link Node}. + */ + public String getXmlText(Node xmlNode) { + String data = null; + IStructuredModel model = getModelForRead(); + try { + IStructuredDocument document = getStructuredDocument(); + if (xmlNode instanceof NodeContainer) { + // The easy way to get the source of an SSE XML node. + data = ((NodeContainer) xmlNode).getSource(); + } else if (xmlNode instanceof IndexedRegion && document != null) { + // Try harder. + IndexedRegion region = (IndexedRegion) xmlNode; + int start = region.getStartOffset(); + int end = region.getEndOffset(); + + if (end > start) { + data = document.get(start, end - start); + } + } + } catch (BadLocationException e) { + // the region offset was invalid. ignore. + } finally { + model.releaseFromRead(); + } + return data; + } + + /** * Listen to changes in the underlying XML model in the structured editor. */ private class XmlModelStateListener implements IModelStateListener { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java index 3875896..c23a4d4 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java @@ -534,7 +534,7 @@ public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput, * Helper method that returns a {@link ViewElementDescriptor} for the requested FQCN. * Will return null if we can't find that FQCN or we lack the editor/data/descriptors info. */ - public ViewElementDescriptor getFqcnViewDescritor(String fqcn) { + public ViewElementDescriptor getFqcnViewDescriptor(String fqcn) { ViewElementDescriptor desc = null; AndroidTargetData data = getTargetData(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasSelection.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasSelection.java index 4c29c5f..cd927ec 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasSelection.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasSelection.java @@ -17,12 +17,18 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import com.android.ide.common.api.INode; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; 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.sdklib.SdkConstants; 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}. @@ -70,6 +76,7 @@ import org.eclipse.swt.graphics.Rectangle; /** * 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 mNodeProxy.getParent() == null; @@ -123,7 +130,7 @@ import org.eclipse.swt.graphics.Rectangle; if (mNodeProxy != null) { INode parent = mNodeProxy.getParent(); if (parent instanceof NodeProxy) { - gre.callOnChildSelected(gcWrapper, (NodeProxy)parent, mNodeProxy); + gre.callOnChildSelected(gcWrapper, (NodeProxy) parent, mNodeProxy); } } } @@ -174,4 +181,47 @@ import org.eclipse.swt.graphics.Rectangle; return name; } + + /** + * Gets the XML text from the given selection for a text transfer. + * The returned string can be empty but not null. + */ + /* package */ static String getAsText(LayoutCanvas canvas, List<CanvasSelection> selection) { + StringBuilder sb = new StringBuilder(); + + LayoutEditor layoutEditor = canvas.getLayoutEditor(); + for (CanvasSelection cs : selection) { + CanvasViewInfo vi = cs.getViewInfo(); + UiViewElementNode key = vi.getUiViewKey(); + Node node = key.getXmlNode(); + String t = layoutEditor.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. + */ + /* package */ static SimpleElement[] getAsElements(List<CanvasSelection> items) { + ArrayList<SimpleElement> elements = new ArrayList<SimpleElement>(); + + for (CanvasSelection cs : items) { + CanvasViewInfo vi = cs.getViewInfo(); + + SimpleElement e = vi.toSimpleElement(); + elements.add(e); + } + + return elements.toArray(new SimpleElement[elements.size()]); + } } 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 index f4dce0f..9714b6c 100755 --- 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 @@ -16,7 +16,11 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import com.android.ide.common.api.Rect; +import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; 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.layoutlib.api.ILayoutResult; import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo; @@ -69,7 +73,8 @@ public class CanvasViewInfo implements IPropertySource { this(viewInfo, null /*parent*/, 0 /*parentX*/, 0 /*parentY*/); } - private CanvasViewInfo(ILayoutViewInfo viewInfo, CanvasViewInfo parent, int parentX, int parentY) { + private CanvasViewInfo(ILayoutViewInfo viewInfo, CanvasViewInfo parent, + int parentX, int parentY) { mParent = parent; mName = viewInfo.getName(); @@ -258,4 +263,65 @@ public class CanvasViewInfo implements IPropertySource { 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). + return mUiViewKey == null || mUiViewKey.getUiParent() == null || + mUiViewKey.getUiParent().getUiParent() == null; + } + + /** + * Returns the info represented as a {@link SimpleElement}. + * + * @return A {@link SimpleElement} wrapping this info. + */ + /* package */ SimpleElement toSimpleElement() { + + UiViewElementNode uiNode = getUiViewKey(); + + String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor()); + String parentFqcn = null; + Rect bounds = new Rect(getAbsRect()); + Rect parentBounds = null; + + UiElementNode uiParent = uiNode.getUiParent(); + if (uiParent != null) { + parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor()); + } + if (getParent() != null) { + parentBounds = new Rect(getParent().getAbsRect()); + } + + SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds); + + for (UiAttributeNode attr : uiNode.getUiAttributes()) { + 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; + } + } 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 0000000..00b2518 --- /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,367 @@ +/* + * 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.IDragElement; +import com.android.ide.common.api.IDragElement.IDragAttribute; +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.descriptors.XmlnsAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; +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.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.sdklib.SdkConstants; + +import org.eclipse.jface.action.Action; +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.List; + +/** + * 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) { + this.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. + * + * @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<CanvasSelection> selection) { + SelectionManager.sanitize(selection); + + if (selection.isEmpty()) { + return; + } + + Object[] data = new Object[] { + CanvasSelection.getAsElements(selection), + CanvasSelection.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<CanvasSelection> 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<CanvasSelection> 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 (CanvasSelection cs : selection) { + CanvasViewInfo vi = cs.getViewInfo(); + if (vi != null && vi.getParent() != null) { + if (title == null) { + title = vi.getParent().getName(); + } else if (!title.equals(vi.getParent().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.getLayoutEditor().wrapUndoEditXmlModel(title, new Runnable() { + public void run() { + for (CanvasSelection cs : selection) { + CanvasViewInfo vi = cs.getViewInfo(); + // You can't delete the root element + if (vi != null && !vi.isRoot()) { + UiViewElementNode ui = vi.getUiViewKey(); + 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<CanvasSelection> selection) { + + SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance(); + 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); + CanvasViewInfo target = lastRoot; + if (selection.size() > 0) { + CanvasSelection cs = selection.get(0); + target = cs.getViewInfo(); + } + + NodeProxy targetNode = mCanvas.getNodeFactory().create(target); + + mCanvas.getRulesEngine().callOnPaste(targetNode, pasted); + } + + /** + * 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(String)} 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 LayoutEditor layoutEditor = mCanvas.getLayoutEditor(); + final UiDocumentNode uiDoc = layoutEditor.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 = layoutEditor.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); + + layoutEditor.wrapUndoEditXmlModel(title, new Runnable() { + public void run() { + UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc); + + // A root node requires the Android XMLNS + uiNew.setAttributeValue( + "android", //$NON-NLS-1$ + XmlnsAttributeDescriptor.XMLNS_URI, + SdkConstants.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 = + layoutEditor.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 0000000..e90371f --- /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,170 @@ +/* + * 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.MouseEvent; +import org.eclipse.swt.events.MouseListener; + +/** + * 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 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) { + this.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; + } +} 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 0000000..bb3be7f --- /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 index fb7ccfa..66f2327 100755 --- 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 @@ -215,7 +215,7 @@ import java.util.regex.Pattern; final TreeMap<String, ArrayList<MenuAction>> outActionsMap, final TreeMap<String, MenuAction.Group> outGroupsMap) { int maxMenuSelection = 0; - for (CanvasSelection selection : mCanvas.getCanvasSelections()) { + for (CanvasSelection selection : mCanvas.getSelectionManager().getSelections()) { List<MenuAction> viewActions = null; if (selection != null) { CanvasViewInfo vi = selection.getViewInfo(); 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 index 23b4cbe..fd41a0f 100755 --- 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 @@ -38,7 +38,7 @@ 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> + * <p/> * The actual wrapped GC object is only non-null during the context of a paint operation. */ public class GCWrapper implements IGraphics { @@ -212,6 +212,9 @@ public class GCWrapper implements IGraphics { case LINE_DASHDOTDOT: swtStyle = SWT.LINE_DASHDOTDOT; break; + default: + assert false : style; + break; } if (swtStyle != 0) { @@ -422,12 +425,12 @@ public class GCWrapper implements IGraphics { mCurrentStyle = swtStyle; } - /** Use the stroke alpha for subsequent drawing operations */ + /** Uses the stroke alpha for subsequent drawing operations. */ private void useStrokeAlpha() { mGc.setAlpha(mCurrentStyle.getStrokeAlpha()); } - /** Use the fill alpha for subsequent drawing operations */ + /** Uses the fill alpha for subsequent drawing operations. */ private void useFillAlpha() { mGc.setAlpha(mCurrentStyle.getFillAlpha()); } 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 0000000..c65655a --- /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,138 @@ +/* + * 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.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 & 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 + * & drop, so various gestures will differ in whether they use it. + * In particular, you should use drag & drop if your gesture should: + * <ul> + * <li> Show a native drag & 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 & drop if your gesture should: + * <ul> + * <li> Continue updating itself even when the mouse cursor leaves the + * canvas window (in a drag & 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 & 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) { + this.mStart = pos; + this.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, + */ + public void keyPressed(KeyEvent event) { + } + + /** + * Handles a key release during the gesture. + * + * @param event The SWT event for the key release, + */ + public void keyReleased(KeyEvent event) { + } +} 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 0000000..2bba581 --- /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,599 @@ +/* + * 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.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.TypedEvent; +import org.eclipse.swt.graphics.Device; +import org.eclipse.swt.graphics.GC; + +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(); + + /** + * 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; + + /** + * Constructs a new {@link GestureManager} for the given + * {@link LayoutCanvas}. + * + * @param canvas The canvas which controls this {@link GestureManager} + */ + public GestureManager(LayoutCanvas canvas) { + this.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); + } + } + + /** + * Returns the {@link DropTargetListener} used by the GestureManager. This + * is a bit leaky, but the Outline is reusing all this code... This should + * be separated out. + */ + /* package */DropTargetListener getDropTargetListener() { + return mDropListener; + } + + /** + * Returns the {@link DragSourceListener} used by the GestureManager. This + * is a bit leaky, but the Outline is reusing all this code... This should + * be separated out. + */ + /* package */DragSourceListener getDragSourceListener() { + return mDragSourceListener; + } + + /** + * 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 (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; + } + } + + /** + * Helper class which implements the {@link MouseMoveListener}, + * {@link MouseListener} and {@link KeyListener} interfaces. + */ + private class Listener implements MouseMoveListener, MouseListener, KeyListener { + + // --- MouseMoveListener --- + + public void mouseMove(MouseEvent e) { + mLastMouseX = e.x; + mLastMouseY = e.y; + mLastStateMask = e.stateMask; + + if ((e.stateMask & SWT.BUTTON_MASK) != 0) { + if (mCurrentGesture != null) { + ControlPoint controlPoint = ControlPoint.create(mCanvas, e); + updateMouse(controlPoint, e); + mCanvas.redraw(); + } + } else { + mCanvas.hover(e); + } + } + + // --- MouseListener --- + + public void mouseUp(MouseEvent e) { + if (mCurrentGesture == null) { + // Just a click, select + mCanvas.getSelectionManager().select(e); + } + finishGesture(ControlPoint.create(mCanvas, e), false); + mCanvas.redraw(); + } + + public void mouseDown(MouseEvent e) { + mLastMouseX = e.x; + mLastMouseY = e.y; + mLastStateMask = e.stateMask; + + // Not yet used. Should be, for Mac and Linux. + } + + public void mouseDoubleClick(MouseEvent e) { + mCanvas.showXml(e); + } + + // --- KeyListener --- + + public void keyPressed(KeyEvent e) { + if (mCurrentGesture != null) { + mCurrentGesture.keyPressed(e); + } else { + if (e.keyCode == SWT.ESC) { + // It appears that SWT does NOT (on the Mac) pass any + // key strokes other than modifier keys when the mouse + // button is pressed!! + ControlPoint controlPoint = ControlPoint.create(mCanvas, + mLastMouseX, mLastMouseY); + finishGesture(controlPoint, true); + return; + } + } + } + + public void keyReleased(KeyEvent e) { + if (mCurrentGesture != null) { + mCurrentGesture.keyReleased(e); + } + } + + } + + /** Listener for Drag & Drop events. */ + private class CanvasDropListener implements DropTargetListener { + public CanvasDropListener() { + } + + /** + * The cursor has entered the drop target boundaries. {@inheritDoc} + */ + public void dragEnter(DropTargetEvent event) { + 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} + */ + 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} + */ + public void dragLeave(DropTargetEvent event) { + if (mCurrentGesture instanceof DropGesture) { + DropGesture dropGesture = (DropGesture) mCurrentGesture; + dropGesture.dragLeave(event); + finishGesture(ControlPoint.create(mCanvas, event), true); + mZombieGesture = dropGesture; + } + } + + /** + * The drop is about to be performed. The drop target is given a last + * chance to change the nature of the drop. {@inheritDoc} + */ + 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} + */ + 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} + */ + 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<CanvasSelection> mDragSelection = new ArrayList<CanvasSelection>(); + + 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} + */ + public void dragStart(DragSourceEvent e) { + // 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<CanvasSelection> selections = mCanvas.getSelectionManager().getSelections(); + mDragSelection.clear(); + + if (!selections.isEmpty()) { + // Is the cursor on top of a selected element? + LayoutPoint p = LayoutPoint.create(mCanvas, e); + + boolean insideSelection = false; + + for (CanvasSelection cs : selections) { + if (!cs.isRoot() && cs.getRect().contains(p.x, p.y)) { + insideSelection = true; + break; + } + } + + if (!insideSelection) { + CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p); + if (vi != null && !vi.isRoot()) { + mCanvas.getSelectionManager().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 (CanvasSelection cs : selections) { + if (!cs.isRoot()) { + mDragSelection.add(cs); + } + } + } + } + } + + // If you are dragging a non-selected item, select it + if (mDragSelection.isEmpty()) { + LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout(); + CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p); + if (vi != null && !vi.isRoot()) { + mCanvas.getSelectionManager().selectSingle(vi); + mDragSelection.addAll(selections); + } + } + + SelectionManager.sanitize(mDragSelection); + + e.doit = !mDragSelection.isEmpty(); + if (e.doit) { + mDragElements = CanvasSelection.getAsElements(mDragSelection); + GlobalCanvasDragInfo.getInstance().startDrag(mDragElements, + mDragSelection.toArray(new CanvasSelection[mDragSelection.size()]), + mCanvas, new Runnable() { + public void run() { + mCanvas.getClipboardSupport().deleteSelection("Remove", + mDragSelection); + } + }); + } + + // If you drag on the -background-, we make that into a marquee + // selection + if (!e.doit || (mDragSelection.size() == 1 && mDragSelection.get(0).isRoot())) { + boolean toggle = (mLastStateMask & (SWT.CTRL | SWT.SHIFT | SWT.COMMAND)) != 0; + startGesture(ControlPoint.create(mCanvas, e), + new MarqueeGesture(mCanvas, toggle), mLastStateMask); + e.detail = DND.DROP_NONE; + e.doit = false; + } else { + // Otherwise, the drag means you are moving something + startGesture(ControlPoint.create(mCanvas, e), new MoveGesture(mCanvas), 0); + } + + // 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} + */ + public void dragSetData(DragSourceEvent e) { + if (TextTransfer.getInstance().isSupportedType(e.dataType)) { + e.data = CanvasSelection.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. + */ + 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.redraw(); + } + } +} 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 index d49ff40..330e113 100755 --- 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 @@ -31,10 +31,10 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; * 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 tragged FQCN won't be registered here + * in a <em>separate</em> instance of Eclipse, the dragged FQCN won't be registered here * and will be null. */ -class GlobalCanvasDragInfo { +final class GlobalCanvasDragInfo { private static final GlobalCanvasDragInfo sInstance = new GlobalCanvasDragInfo(); 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 index 253ceed..a0fbac6 100755 --- 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 @@ -462,7 +462,7 @@ public class GraphicalEditorPart extends EditorPart * @param xmlNode The Node whose element we want to select */ public void select(Node xmlNode) { - mCanvasViewer.getCanvas().select(xmlNode); + mCanvasViewer.getCanvas().getSelectionManager().select(xmlNode); } /** 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 0000000..9a39427 --- /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,138 @@ +/* + * 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 HoverOverlay} paints an optional hover on top of the layout, + * highlighting the currently hovered view. + */ +public class HoverOverlay extends Overlay { + /** 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; + + /** Vertical scaling & scrollbar information. */ + private ScaleInfo mVScale; + + /** Horizontal scaling & scrollbar information. */ + private ScaleInfo 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 hScale The {@link ScaleInfo} to use to transfer horizontal layout + * coordinates to screen coordinates. + * @param vScale The {@link ScaleInfo} to use to transfer vertical layout + * coordinates to screen coordinates. + */ + public HoverOverlay(ScaleInfo hScale, ScaleInfo vScale) { + super(); + this.mHScale = hScale; + this.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()); + } + } + + @Override + public void dispose() { + if (mHoverStrokeColor != null) { + mHoverStrokeColor.dispose(); + mHoverStrokeColor = null; + } + + if (mHoverFillColor != null) { + mHoverFillColor.dispose(); + mHoverFillColor = 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); + + if (mHoverStrokeColor != null) { + int oldAlpha = gc.getAlpha(); + gc.setForeground(mHoverStrokeColor); + gc.setLineStyle(SwtDrawingStyle.HOVER.getLineStyle()); + gc.setAlpha(SwtDrawingStyle.HOVER.getStrokeAlpha()); + gc.drawRectangle(x, y, w, h); + gc.setAlpha(oldAlpha); + } + + if (mHoverFillColor != null) { + int oldAlpha = gc.getAlpha(); + gc.setAlpha(SwtDrawingStyle.HOVER.getFillAlpha()); + gc.setBackground(mHoverFillColor); + 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/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 0000000..4817eb8 --- /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,189 @@ +/* + * 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.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.Raster; + +/** + * The {@link ImageOverlay} class renders an image as an overlay. + */ +public class ImageOverlay extends Overlay { + /** Current background image. Null when there's no image. */ + private Image mImage; + + /** The associated {@link LayoutCanvas}. */ + private LayoutCanvas mCanvas; + + /** Vertical scaling & scrollbar information. */ + private ScaleInfo mVScale; + + /** Horizontal scaling & scrollbar information. */ + private ScaleInfo 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, ScaleInfo hScale, ScaleInfo vScale) { + this.mCanvas = canvas; + this.mHScale = hScale; + this.mVScale = vScale; + } + + @Override + public void create(Device device) { + super.create(device); + } + + @Override + public void dispose() { + if (mImage != null) { + mImage.dispose(); + mImage = 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. + * @return The corresponding SWT image, or null. + */ + public Image setImage(BufferedImage awtImage) { + if (mImage != null) { + mImage.dispose(); + } + if (awtImage == null) { + mImage = null; + + } else { + int width = awtImage.getWidth(); + int height = awtImage.getHeight(); + + Raster raster = awtImage.getData(new java.awt.Rectangle(width, height)); + int[] imageDataBuffer = ((DataBufferInt) raster.getDataBuffer()).getData(); + + ImageData imageData = new ImageData(width, height, 32, new PaletteData(0x00FF0000, + 0x0000FF00, 0x000000FF)); + + imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0); + + mImage = new Image(mCanvas.getDisplay(), imageData); + } + + return mImage; + } + + @Override + public void paint(GC gc) { + if (mImage != null) { + boolean valid = mCanvas.getViewHierarchy().isValid(); + if (!valid) { + gc_setAlpha(gc, 128); // half-transparent + } + + ScaleInfo hi = mHScale; + ScaleInfo vi = mVScale; + + // we only anti-alias when reducing the image size. + int oldAlias = -2; + if (hi.getScale() < 1.0) { + oldAlias = gc_setAntialias(gc, SWT.ON); + } + + gc.drawImage( + mImage, + 0, // srcX + 0, // srcY + hi.getImgSize(), // srcWidth + vi.getImgSize(), // srcHeight + hi.translate(0), // destX + vi.translate(0), // destY + hi.getScalledImgSize(), // destWidth + vi.getScalledImgSize()); // 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; + } + } + +} 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 index ebb053a..c97013b 100755 --- 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 @@ -16,31 +16,21 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; -import com.android.ide.common.api.IDragElement; import com.android.ide.common.api.INode; import com.android.ide.common.api.Point; -import com.android.ide.common.api.Rect; -import com.android.ide.common.api.IDragElement.IDragAttribute; 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.AttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; 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.RulesEngine; 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.UiDocumentNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; import com.android.layoutlib.api.ILayoutResult; -import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo; import com.android.sdklib.SdkConstants; -import org.eclipse.core.runtime.ListenerList; -import org.eclipse.gef.ui.parts.TreeViewer; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.ActionContributionItem; import org.eclipse.jface.action.IAction; @@ -48,79 +38,39 @@ import org.eclipse.jface.action.IContributionItem; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; -import org.eclipse.jface.text.BadLocationException; -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.swt.SWT; -import org.eclipse.swt.SWTException; -import org.eclipse.swt.dnd.Clipboard; 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.DropTargetListener; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; -import org.eclipse.swt.dnd.TransferData; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; 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.PaintEvent; import org.eclipse.swt.events.PaintListener; -import org.eclipse.swt.events.SelectionAdapter; -import org.eclipse.swt.events.SelectionEvent; -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.PaletteData; 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.Event; import org.eclipse.swt.widgets.Menu; -import org.eclipse.swt.widgets.ScrollBar; import org.eclipse.ui.IActionBars; import org.eclipse.ui.actions.ActionFactory; import org.eclipse.ui.actions.ContributionItemFactory; import org.eclipse.ui.actions.TextActionHandler; import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction; import org.eclipse.ui.internal.ide.IDEWorkbenchMessages; -import org.eclipse.ui.internal.registry.ViewDescriptor; import org.eclipse.ui.views.contentoutline.IContentOutlinePage; -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.xml.core.internal.document.NodeContainer; import org.w3c.dom.Node; -import java.awt.image.BufferedImage; -import java.awt.image.DataBufferInt; -import java.awt.image.Raster; -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; - /** * Displays the image rendered by the {@link GraphicalEditorPart} and handles * the interaction with the widgets. @@ -129,25 +79,15 @@ import java.util.Set; * actually uses the {@link LayoutCanvasViewer}, which is a JFace viewer wrapper * around this control. * <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 - * control so that it can invoke its own fireSelectionChanged when the control's - * selection changes. + * The LayoutCanvas contains the painting logic for the canvas. Selection, + * clipboard, view management etc. is handled in separate helper classes. * * @since GLE2 */ -/* - * TODO list: - * - gray on error, keep select but disable d'n'd. - * - context menu: enum clear, flag values, toggles as tri-states - * - context menu: impl custom layout width/height - * - properly handle custom views - */ -class LayoutCanvas extends Canvas implements ISelectionProvider { +@SuppressWarnings("restriction") // For WorkBench "Show In" support +class LayoutCanvas extends Canvas { - private final static boolean DEBUG = false; + private static final boolean DEBUG = false; /* package */ static final String PREFIX_CANVAS_ACTION = "canvas_action_"; @@ -157,43 +97,6 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { /** The Rules Engine, associated with the current project. */ private RulesEngine mRulesEngine; - /** SWT clipboard instance. */ - private Clipboard mClipboard; - - /** - * The CanvasViewInfo root created by the last call to {@link #setResult(ILayoutResult)} - * 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 #setResult(ILayoutResult)} provided a valid {@link ILayoutResult}. - * <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; - - /** Current background image. Null when there's no image. */ - private Image mImage; - - /** The current selection list. The list is never null, however it can be empty. */ - private final LinkedList<CanvasSelection> mSelections = new LinkedList<CanvasSelection>(); - - /** An unmodifiable view of {@link #mSelections}. */ - private List<CanvasSelection> mUnmodifiableSelection; - - /** CanvasSelection border color. Do not dispose, it's a system color. */ - private Color mSelectionFgColor; - /** 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; @@ -204,36 +107,12 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { /** Current hover view info. Null when no mouse hover. */ private CanvasViewInfo mHoverViewInfo; - /** 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; - - /** 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; - - /** Outline color. Must be disposed, it's NOT a system color. */ - private Color mOutlineColor; - - /** - * 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; - /** When true, always display the outline of all views. */ private boolean mShowOutline; /** Drop target associated with this composite. */ private DropTarget mDropTarget; - /** Drop listener, with feedback from current drop */ - private CanvasDropListener mDropListener; - /** Factory that can create {@link INode} proxies. */ private final NodeFactory mNodeFactory = new NodeFactory(); @@ -246,9 +125,6 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { /** Drag source associated with this canvas. */ private DragSource mDragSource; - /** List of clients listening to selection changes. */ - private final ListenerList mSelectionListeners = new ListenerList(); - /** * The current Outline Page, to set its model. * It isn't possible to call OutlinePage2.dispose() in this.dispose(). @@ -258,10 +134,6 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { **/ private OutlinePage2 mOutlinePage; - /** Barrier set when updating the selection to prevent from recursively - * invoking ourselves. */ - private boolean mInsideUpdateSelection; - /** Delete action for the Edit or context menu. */ private Action mDeleteAction; @@ -280,8 +152,34 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { /** Root of the context menu. */ private MenuManager mMenuManager; - private CanvasDragSourceListener mDragSourceListener; + /** 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 the mouse hover. */ + private HoverOverlay mHoverOverlay; + + /** The overlay which paints the selection. */ + private SelectionOverlay mSelectionOverlay; + + /** The overlay which paints the rendered layout image. */ + private ImageOverlay mImageOverlay; + + /** + * Gesture Manager responsible for identifying mouse, keyboard and drag and + * drop events. + */ + private final GestureManager mGestureManager = new GestureManager(this); + + /** + * Native clipboard support. + */ + private ClipboardSupport mClipboardSupport; public LayoutCanvas(LayoutEditor layoutEditor, RulesEngine rulesEngine, @@ -291,25 +189,25 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { mLayoutEditor = layoutEditor; mRulesEngine = rulesEngine; - mClipboard = new Clipboard(parent.getDisplay()); - - mHScale = new ScaleInfo(getHorizontalBar()); - mVScale = new ScaleInfo(getVerticalBar()); + mClipboardSupport = new ClipboardSupport(this, parent); + mHScale = new ScaleInfo(this, getHorizontalBar()); + mVScale = new ScaleInfo(this, getVerticalBar()); mGCWrapper = new GCWrapper(mHScale, mVScale); - Display d = getDisplay(); - mSelectionFgColor = new Color(d, SwtDrawingStyle.SELECTION.getStrokeColor()); - if (SwtDrawingStyle.HOVER.getStrokeColor() != null) { - mHoverStrokeColor = new Color(d, SwtDrawingStyle.HOVER.getStrokeColor()); - } - if (SwtDrawingStyle.HOVER.getFillColor() != null) { - mHoverFillColor = new Color(d, SwtDrawingStyle.HOVER.getFillColor()); - } - mOutlineColor = new Color(d, SwtDrawingStyle.OUTLINE.getStrokeColor()); + Display display = getDisplay(); + mFont = display.getSystemFont(); - mFont = d.getSystemFont(); + // --- Set up graphic overlays + // mOutlineOverlay is initialized lazily + mHoverOverlay = new HoverOverlay(mHScale, mVScale); + mHoverOverlay.create(display); + mSelectionOverlay = new SelectionOverlay(); + mSelectionOverlay.create(display); + mImageOverlay = new ImageOverlay(this, mHScale, mVScale); + mImageOverlay.create(display); + // --- Set up listeners addPaintListener(new PaintListener() { public void paintControl(PaintEvent e) { onPaint(e); @@ -325,26 +223,6 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { } }); - addMouseMoveListener(new MouseMoveListener() { - public void mouseMove(MouseEvent e) { - onMouseMove(e); - } - }); - - addMouseListener(new MouseListener() { - public void mouseUp(MouseEvent e) { - onMouseUp(e); - } - - public void mouseDown(MouseEvent e) { - onMouseDown(e); - } - - public void mouseDoubleClick(MouseEvent e) { - onDoubleClick(e); - } - }); - addKeyListener(new KeyListener() { public void keyPressed(KeyEvent e) { @@ -365,11 +243,14 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { // --- setup drag'n'drop --- // DND Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html - mDropListener = new CanvasDropListener(this); - mDropTarget = createDropTarget(this, mDropListener); + mDropTarget = createDropTarget(this); + mDragSource = createDragSource(this); + mGestureManager.registerListeners(mDragSource, mDropTarget); - mDragSourceListener = new CanvasDragSourceListener(); - mDragSource = createDragSource(this, mDragSourceListener); + if (mLayoutEditor == null) { + // TODO: In another CL we should use EasyMock/objgen to provide an editor. + return; // Unit test + } // --- setup context menu --- setupGlobalActionHandlers(); @@ -387,25 +268,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { public void dispose() { super.dispose(); - if (mSelectionFgColor != null) { - mSelectionFgColor.dispose(); - mSelectionFgColor = null; - } - - if (mOutlineColor != null) { - mOutlineColor.dispose(); - mOutlineColor = null; - } - - if (mHoverStrokeColor != null) { - mHoverStrokeColor.dispose(); - mHoverStrokeColor = null; - } - - if (mHoverFillColor != null) { - mHoverFillColor.dispose(); - mHoverFillColor = null; - } + mGestureManager.unregisterListeners(mDragSource, mDropTarget); if (mDropTarget != null) { mDropTarget.dispose(); @@ -422,41 +285,35 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { mDragSource = null; } - if (mClipboard != null) { - mClipboard.dispose(); - mClipboard = null; - } - - if (mImage != null) { - mImage.dispose(); - mImage = null; + if (mClipboardSupport != null) { + mClipboardSupport.dispose(); + mClipboardSupport = null; } if (mGCWrapper != null) { mGCWrapper.dispose(); mGCWrapper = null; } - } - /** - * Returns true when the last {@link #setResult(ILayoutResult)} provided a valid - * {@link ILayoutResult}. - * <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. - */ - /* package */ boolean isResultValid() { - return mIsResultValid; - } + if (mOutlineOverlay != null) { + mOutlineOverlay.dispose(); + mOutlineOverlay = null; + } - /** - * Returns true if the last valid content of the canvas represents an empty document. - */ - /* package */ boolean isEmptyDocument() { - return mLastValidViewInfoRoot == null; + if (mHoverOverlay != null) { + mHoverOverlay.dispose(); + mHoverOverlay = null; + } + + if (mSelectionOverlay != null) { + mSelectionOverlay.dispose(); + mSelectionOverlay = null; + } + + if (mImageOverlay != null) { + mImageOverlay.dispose(); + mImageOverlay = null; + } } /** Returns the Rules Engine, associated with the current project. */ @@ -482,7 +339,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { * This is used by {@link OutlinePage2} to delegate drag source events. */ /* package */ DragSourceListener getDragListener() { - return mDragSourceListener; + return mGestureManager.getDragSourceListener(); } /** @@ -490,21 +347,16 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { * This is used by {@link OutlinePage2} to delegate drop target events. */ /* package */ DropTargetListener getDropListener() { - return mDropListener; + return mGestureManager.getDropTargetListener(); } /** - * Returns the native {@link CanvasSelection} list. + * Returns the GCWrapper used to paint view rules. * - * @return An immutable list of {@link CanvasSelection}. Can be empty but not null. - * @see #getSelection() {@link #getSelection()} to retrieve a {@link TreeViewer} - * compatible {@link ISelection}. + * @return The GCWrapper used to paint view rules */ - /* package */ List<CanvasSelection> getCanvasSelections() { - if (mUnmodifiableSelection == null) { - mUnmodifiableSelection = Collections.unmodifiableList(mSelections); - } - return mUnmodifiableSelection; + /* package */ GCWrapper getGcWrapper() { + return mGCWrapper; } /** @@ -515,6 +367,58 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { } /** + * Returns the horizontal {@link ScaleInfo} transform object, which can map + * a layout point into a control point. + * + * @return A {@link ScaleInfo} for mapping between layout and control + * coordinates in the horizontal dimension. + */ + /* package */ ScaleInfo getHorizontalTransform() { + return mHScale; + } + + /** + * Returns the vertical {@link ScaleInfo} transform object, which can map a + * layout point into a control point. + * + * @return A {@link ScaleInfo} for mapping between layout and control + * coordinates in the vertical dimension. + */ + /* package */ ScaleInfo getVerticalTransform() { + return mVScale; + } + + /** + * 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; + } + + /** * 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. * @@ -526,47 +430,17 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { */ /* package */ void setResult(ILayoutResult result) { // disable any hover - mHoverRect = null; + clearHover(); - mIsResultValid = (result != null && result.getSuccess() == ILayoutResult.SUCCESS); + mViewHierarchy.setResult(result); + if (mViewHierarchy.isValid() && result != null) { + Image image = mImageOverlay.setImage(result.getImage()); - if (mIsResultValid && result != null) { - ILayoutViewInfo root = result.getRootView(); - if (root == null) { - mLastValidViewInfoRoot = null; - } else { - mLastValidViewInfoRoot = new CanvasViewInfo(result.getRootView()); - } - setImage(result.getImage()); - - updateNodeProxies(mLastValidViewInfoRoot); - mOutlinePage.setModel(mLastValidViewInfoRoot); - - // Check if the selection is still the same (based on the object keys) - // and eventually recompute their bounds. - for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) { - CanvasSelection s = it.next(); - - // Check if the selected object still exists - Object key = s.getViewInfo().getUiViewKey(); - CanvasViewInfo vi = findViewInfoKey(key, mLastValidViewInfoRoot); - - // 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) { - it.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory)); - } - } - fireSelectionChanged(); - - // remove the current alternate selection views - mAltSelection = null; + mOutlinePage.setModel(mViewHierarchy.getRoot()); - if (mImage != null) { - mHScale.setSize(mImage.getImageData().width, getClientArea().width); - mVScale.setSize(mImage.getImageData().height, getClientArea().height); + if (image != null) { + mHScale.setSize(image.getImageData().width, getClientArea().width); + mVScale.setSize(image.getImageData().height, getClientArea().height); } // Pre-load the android.view.View rule in the Rules Engine. Doing it here means @@ -596,157 +470,19 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { } /** - * Transforms a point, expressed in SWT display coordinates - * (e.g. from a Drag'n'Drop {@link Event}, not local {@link Control} coordinates) - * into the canvas' image coordinates according to the current zoom and scroll. - * - * @param displayX X in SWT display coordinates - * @param displayY Y in SWT display coordinates - * @return A new {@link Point} in canvas coordinates - */ - /* package */ Point displayToCanvasPoint(int displayX, int displayY) { - // convert screen coordinates to local SWT control coordinates - org.eclipse.swt.graphics.Point p = this.toControl(displayX, displayY); - - int x = mHScale.inverseTranslate(p.x); - int y = mVScale.inverseTranslate(p.y); - return new Point(x, y); - } - - /** - * Transforms a point, expressed in canvas coordinates, into "client" coordinates - * relative to the control (and not relative to the display.) + * 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) */ - /* package */ Point canvasToControlPoint(int canvasX, int canvasY) { + /* package */ Point layoutToControlPoint(int canvasX, int canvasY) { int x = mHScale.translate(canvasX); int y = mVScale.translate(canvasY); return new Point(x, y); } - //---- - // Implementation of ISelectionProvider - - /** - * Returns a {@link TreeSelection} compatible with a TreeViewer - * where each {@link TreePath} item is actually a {@link CanvasViewInfo}. - */ - public ISelection getSelection() { - if (mSelections.isEmpty()) { - return TreeSelection.EMPTY; - } - - ArrayList<TreePath> paths = new ArrayList<TreePath>(); - - for (CanvasSelection cs : mSelections) { - CanvasViewInfo vi = cs.getViewInfo(); - if (vi != null) { - ArrayList<Object> segments = new ArrayList<Object>(); - while (vi != null) { - segments.add(0, vi); - vi = vi.getParent(); - } - paths.add(new TreePath(segments.toArray())); - } - } - - return new TreeSelection(paths.toArray(new TreePath[paths.size()])); - } - - /** - * 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. - */ - public void setSelection(ISelection selection) { - if (mInsideUpdateSelection) { - return; - } - - 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; - redraw(); - } - return; - } - - boolean changed = false; - - // Create a list of all currently selected view infos - Set<CanvasViewInfo> oldSelected = new HashSet<CanvasViewInfo>(); - for (CanvasSelection 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 de-select 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( - new CanvasSelection(newVi, mRulesEngine, mNodeFactory)); - changed = true; - } - } - } - - // De-select old selected items that are not in the new one - for (CanvasViewInfo vi : oldSelected) { - deselect(vi); - changed = true; - } - - if (changed) { - redraw(); - updateMenuActions(); - } - - } - } finally { - mInsideUpdateSelection = false; - } - } - - public void addSelectionChangedListener(ISelectionChangedListener listener) { - mSelectionListeners.add(listener); - } - - public void removeSelectionChangedListener(ISelectionChangedListener listener) { - mSelectionListeners.remove(listener); - } - /** * Returns the action for the context menu corresponding to the given action id. * <p/> @@ -776,210 +512,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { return null; } - //--- - - /** - * Helper class to convert between control pixel coordinates and canvas coordinates. - * Takes care of the zooming and offset of the canvas. - */ - private class ScaleInfo implements ICanvasTransform { - /** Canvas image size (original, before zoom), in pixels */ - private int mImgSize; - - /** Client size, in pixels */ - private int mClientSize; - - /** Left-top offset in client pixel coordinates */ - private int mTranslate; - - /** Scaling factor, > 0 */ - private double mScale; - - /** Scrollbar widget */ - ScrollBar mScrollbar; - - public ScaleInfo(ScrollBar scrollbar) { - 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(); - redraw(); - } - }); - } - - /** - * Sets the new scaling factor. Recomputes scrollbars. - * @param scale Scaling factor, > 0. - */ - public void setScale(double scale) { - if (mScale != scale) { - mScale = scale; - resizeScrollbar(); - } - } - - /** Returns current scaling factor. */ - public double getScale() { - return mScale; - } - - /** Returns Canvas image size (original, before zoom), in pixels. */ - public int getImgSize() { - return mImgSize; - } - - /** Returns the scaled image size in pixels. */ - public int getScalledImgSize() { - return (int) (mImgSize * mScale); - } - - /** Changes the size of the canvas image and the client size. Recomputes scrollbars. */ - public void setSize(int imgSize, int clientSize) { - mImgSize = imgSize; - setClientSize(clientSize); - } - - /** Changes the size of the client size. Recomputes scrollbars. */ - public void setClientSize(int clientSize) { - mClientSize = clientSize; - resizeScrollbar(); - } - - private void resizeScrollbar() { - // scaled image size - int sx = (int) (mImgSize * mScale); - - // actual client area is always reduced by the margins - int cx = mClientSize - 2 * IMAGE_MARGIN; - - if (sx < cx) { - mScrollbar.setEnabled(false); - } else { - mScrollbar.setEnabled(true); - - // max scroll value is the scaled image size - // thumb value is the actual viewable area out of the scaled img size - mScrollbar.setMaximum(sx); - mScrollbar.setThumb(cx); - } - } - - public int translate(int canvasX) { - return IMAGE_MARGIN - mTranslate + (int)(mScale * canvasX); - } - - public int scale(int canwasW) { - return (int)(mScale * canwasW); - } - - public int inverseTranslate(int screenX) { - return (int) ((screenX - IMAGE_MARGIN + mTranslate) / mScale); - } - } - - /** - * 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.getUiViewKey(); - - if (key != null) { - mNodeFactory.create(vi); - } - - for (CanvasViewInfo child : vi.getChildren()) { - updateNodeProxies(child); - } - } - - /** - * Sets the image of the last *successful* rendering. - * Converts the AWT image into an SWT image. - * <p/> - * The image *can* be null, which is the case when we are dealing with an empty document. - */ - private void setImage(BufferedImage awtImage) { - if (mImage != null) { - mImage.dispose(); - } - if (awtImage == null) { - mImage = null; - - } else { - int width = awtImage.getWidth(); - int height = awtImage.getHeight(); - - Raster raster = awtImage.getData(new java.awt.Rectangle(width, height)); - int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData(); - - ImageData imageData = new ImageData(width, height, 32, - new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF)); - - imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0); - - mImage = new Image(getDisplay(), imageData); - } - } - - /** - * 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; - } - } + //--------------- /** * Paints the canvas in response to paint events. @@ -989,132 +522,47 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { gc.setFont(mFont); mGCWrapper.setGC(gc); try { + mImageOverlay.paint(gc); - if (mImage != null) { - if (!mIsResultValid) { - gc_setAlpha(gc, 128); // half-transparent - } - - ScaleInfo hi = mHScale; - ScaleInfo vi = mVScale; - - // we only anti-alias when reducing the image size. - int oldAlias = -2; - if (hi.getScale() < 1.0) { - oldAlias = gc_setAntialias(gc, SWT.ON); - } - - gc.drawImage(mImage, - 0, // srcX - 0, // srcY - hi.getImgSize(), // srcWidth - vi.getImgSize(), // srcHeight - hi.translate(0), // destX - vi.translate(0), // destY - hi.getScalledImgSize(), // destWidth - vi.getScalledImgSize() // destHeight - ); - - if (oldAlias != -2) { - gc_setAntialias(gc, oldAlias); - } - - if (!mIsResultValid) { - gc_setAlpha(gc, 255); // opaque + if (mShowOutline) { + if (mOutlineOverlay == null) { + mOutlineOverlay = new OutlineOverlay(mViewHierarchy, mHScale, mVScale); + mOutlineOverlay.create(getDisplay()); } + mOutlineOverlay.paint(gc); } - if (mShowOutline && mLastValidViewInfoRoot != null) { - gc.setForeground(mOutlineColor); - gc.setLineStyle(SwtDrawingStyle.OUTLINE.getLineStyle()); - int oldAlpha = gc.getAlpha(); - gc.setAlpha(SwtDrawingStyle.OUTLINE.getStrokeAlpha()); - drawOutline(gc, mLastValidViewInfoRoot); - gc.setAlpha(oldAlpha); - } - - if (mHoverRect != null) { - int x = mHScale.translate(mHoverRect.x); - int y = mVScale.translate(mHoverRect.y); - int w = mHScale.scale(mHoverRect.width); - int h = mVScale.scale(mHoverRect.height); - - if (mHoverStrokeColor != null) { - int oldAlpha = gc.getAlpha(); - gc.setForeground(mHoverStrokeColor); - gc.setLineStyle(SwtDrawingStyle.HOVER.getLineStyle()); - gc.setAlpha(SwtDrawingStyle.HOVER.getStrokeAlpha()); - gc.drawRectangle(x, y, w, h); - gc.setAlpha(oldAlpha); - } - - if (mHoverFillColor != null) { - int oldAlpha = gc.getAlpha(); - gc.setAlpha(SwtDrawingStyle.HOVER.getFillAlpha()); - gc.setBackground(mHoverFillColor); - gc.fillRectangle(x, y, w, h); - gc.setAlpha(oldAlpha); - } - } - - int n = mSelections.size(); - if (n > 0) { - boolean isMultipleSelection = n > 1; - - if (n == 1) { - gc.setForeground(mSelectionFgColor); - mSelections.get(0).paintParentSelection(mRulesEngine, mGCWrapper); - } - - for (CanvasSelection s : mSelections) { - gc.setForeground(mSelectionFgColor); - s.paintSelection(mRulesEngine, mGCWrapper, isMultipleSelection); - } - } - - if (mDropListener != null) { - mDropListener.paintFeedback(mGCWrapper); - } + mHoverOverlay.paint(gc); + mSelectionOverlay.paint(mSelectionManager, gc, mGCWrapper, mRulesEngine); + mGestureManager.paint(gc); } finally { mGCWrapper.setGC(null); } } - 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); - } + /** + * Clears the hover. + */ + /* package */ void clearHover() { + mHoverOverlay.clearHover(); } /** * Hover on top of a known child. */ - private void onMouseMove(MouseEvent e) { - CanvasViewInfo root = mLastValidViewInfoRoot; - - int x = mHScale.inverseTranslate(e.x); - int y = mVScale.inverseTranslate(e.y); + /* package */ void hover(MouseEvent e) { + // Check if a button is pressed; no hovers during drags + if ((e.stateMask & SWT.BUTTON_MASK) != 0) { + clearHover(); + return; + } - CanvasViewInfo vi = findViewInfoAt(x, y); + 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. - if (vi == root) { + if (vi != null && vi.isRoot()) { vi = null; } @@ -1122,10 +570,10 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { mHoverViewInfo = vi; if (vi == null) { - mHoverRect = null; + clearHover(); } else { Rectangle r = vi.getSelectionRect(); - mHoverRect = new Rectangle(r.x, r.y, r.width, r.height); + mHoverOverlay.setHover(r.x, r.y, r.width, r.height); } if (needsUpdate) { @@ -1133,226 +581,19 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { } } - private void onMouseDown(MouseEvent e) { - // Pass, not used yet. We do everything on mouse up. - } - /** - * Performs selection on mouse up (not mouse down). - * <p/> - * Shift key 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). - */ - private void onMouseUp(MouseEvent e) { - - boolean isShift = (e.stateMask & SWT.SHIFT) != 0; - boolean isAlt = (e.stateMask & SWT.ALT) != 0; - - int x = mHScale.inverseTranslate(e.x); - int y = mVScale.inverseTranslate(e.y); - - 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 (!isAlt && !isShift) { - for (CanvasSelection cs : mSelections) { - if (cs.getRect().contains(x, 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 = findViewInfoAt(x, y); - - if (isShift && !isAlt) { - // 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)) { - redraw(); - return; - } - - // otherwise add it. - mSelections.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory)); - fireSelectionChanged(); - redraw(); - } - - } else if (isAlt) { - // 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, findAltViewInfoAt(x, y, mLastValidViewInfoRoot)); - - // 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(new CanvasSelection(vi2, mRulesEngine, mNodeFactory)); - 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(new CanvasSelection(vi2, mRulesEngine, mNodeFactory)); - 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 {@link #redraw()} if the selection changes. - * - * @param vi The new selected item if non-null. Selection becomes empty if null. - */ - private void selectSingle(CanvasViewInfo vi) { - // reset alternate selection if any - mAltSelection = null; - - // 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.clear(); - } - - if (vi != null) { - mSelections.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory)); - } - fireSelectionChanged(); - redraw(); - } - - /** - * 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()}. + * Show the XML element corresponding to the point under the mouse event + * (unless it's a root). * - * @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; - - mSelections.clear(); - if (viewInfos != null) { - for (CanvasViewInfo viewInfo : viewInfos) { - mSelections.add(new CanvasSelection(viewInfo, mRulesEngine, mNodeFactory)); - } - } - - fireSelectionChanged(); - redraw(); - } - - /** - * Select the visual element corresponding to the given XML node - * @param xmlNode The Node whose element we want to select + * @param e A mouse event pointing on the screen whose underlying XML + * element we want to view */ - public void select(Node xmlNode) { - CanvasViewInfo vi = findViewInfoFor(xmlNode); - if (vi != null) { - // 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). - UiViewElementNode key = vi.getUiViewKey(); - if (key != null && key.getUiParent() != null && - key.getUiParent().getUiParent() != null) { - selectSingle(vi); - } - } - } - - /** - * Deselects a view info. - * Returns true if the object was actually selected. - * Callers are responsible for calling redraw() and updateOulineSelection() after. - */ - private boolean deselect(CanvasViewInfo canvasViewInfo) { - if (canvasViewInfo == null) { - return false; - } - - for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) { - CanvasSelection 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<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) { - CanvasSelection s = it.next(); - if (canvasViewInfos.contains(s.getViewInfo())) { - it.remove(); - } - } - } - - private void onDoubleClick(MouseEvent e) { + public void showXml(MouseEvent e) { // Warp to the text editor and show the corresponding XML for the // double-clicked widget - int x = mHScale.inverseTranslate(e.x); - int y = mVScale.inverseTranslate(e.y); - CanvasViewInfo vi = findViewInfoAt(x, y); - if (vi == null) { + LayoutPoint p = ControlPoint.create(this, e).toLayout(); + CanvasViewInfo vi = mViewHierarchy.findViewInfoAt(p); + if (vi == null || vi.isRoot()) { return; } @@ -1365,226 +606,6 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { } } - /** - * Tries to find a child with the same view key in the view info sub-tree. - * Returns null if not found. - */ - private CanvasViewInfo findViewInfoKey(Object viewKey, CanvasViewInfo canvasViewInfo) { - if (canvasViewInfo == null) { - return null; - } - if (canvasViewInfo.getUiViewKey() == viewKey) { - return canvasViewInfo; - } - - // try to find a matching child - for (CanvasViewInfo child : canvasViewInfo.getChildren()) { - CanvasViewInfo v = findViewInfoKey(viewKey, child); - if (v != null) { - return v; - } - } - - return null; - } - - /** - * 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. - */ - /* package */ CanvasViewInfo findViewInfoFor(Node node) { - if (mLastValidViewInfoRoot != null) { - return findViewInfoForNode(node, mLastValidViewInfoRoot); - } else { - return null; - } - } - - /** - * Tries to find a child with the same view XML node in the view info sub-tree. - * Returns null if not found. - */ - private CanvasViewInfo findViewInfoForNode(Node xmlNode, CanvasViewInfo canvasViewInfo) { - if (canvasViewInfo == null) { - return null; - } - if (canvasViewInfo.getXmlNode() == xmlNode) { - return canvasViewInfo; - } - - // Try to find a matching child - for (CanvasViewInfo child : canvasViewInfo.getChildren()) { - CanvasViewInfo v = findViewInfoForNode(xmlNode, child); - if (v != null) { - return v; - } - } - - return null; - } - - - /** - * 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. - */ - /* package */ CanvasViewInfo findViewInfoAt(int x, int y) { - if (mLastValidViewInfoRoot == null) { - return null; - } else { - return findViewInfoAt_Recursive(x, y, 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(int x, int y, CanvasViewInfo canvasViewInfo) { - if (canvasViewInfo == null) { - return null; - } - Rectangle r = canvasViewInfo.getSelectionRect(); - if (r.contains(x, y)) { - - // try to find a matching child first - for (CanvasViewInfo child : canvasViewInfo.getChildren()) { - CanvasViewInfo v = findViewInfoAt_Recursive(x, y, 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. - */ - private List<CanvasViewInfo> findAltViewInfoAt(int x, int y, CanvasViewInfo parent) { - return findAltViewInfoAt_Recursive(x, y, parent, null); - } - - /** - * Internal recursive version of {@link #findAltViewInfoAt(int, int, CanvasViewInfo)}. - * Please don't use directly. - */ - private List<CanvasViewInfo> findAltViewInfoAt_Recursive( - int x, int y, 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(x, 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(x, y)) { - outList.add(child); - } - } - - // finally recurse in the children - for (CanvasViewInfo child : parent.getChildren()) { - r = child.getSelectionRect(); - if (r.contains(x, y)) { - findAltViewInfoAt_Recursive(x, y, 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. - */ - /* package */ CanvasViewInfo findViewInfoFor(INode node) { - if (mLastValidViewInfoRoot != null && node instanceof NodeProxy) { - return findViewInfoKey(((NodeProxy) node).getNode(), mLastValidViewInfoRoot); - } else { - return null; - } - } - - /** - * Used by {@link #onSelectAll()} to add all current view infos to the selection list. - * - * @param canvasViewInfo The root to add. This info and all its children will be added to the - * selection list. - */ - private void selectAllViewInfos(CanvasViewInfo canvasViewInfo) { - if (canvasViewInfo != null) { - mSelections.add(new CanvasSelection(canvasViewInfo, mRulesEngine, mNodeFactory)); - for (CanvasViewInfo vi : canvasViewInfo.getChildren()) { - selectAllViewInfos(vi); - } - } - } - - /** - * 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() { - public void run() { - for (Object listener : mSelectionListeners.getListeners()) { - ((ISelectionChangedListener)listener).selectionChanged(event); - } - } - }); - - // Update menu actions that depend on the selection - updateMenuActions(); - - } finally { - mInsideUpdateSelection = false; - } - } - - //--------------- /** @@ -1593,15 +614,12 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { * This is static with package-access so that {@link OutlinePage2} can also * create an exact copy of the source with the same attributes. */ - /* package */ static DragSource createDragSource( - Control control, - DragSourceListener dragSourceListener) { + /* 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() - } ); - source.addDragListener(dragSourceListener); + }); return source; } @@ -1611,310 +629,15 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { * This is static with package-access so that {@link OutlinePage2} can also * create an exact copy of the drop target with the same attributes. */ - /* package */ static DropTarget createDropTarget( - Control control, - DropTargetListener dropListener) { + /* package */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() } ); - dropTarget.addDropListener(dropListener); + dropTarget.setTransfer(new Transfer[] { + SimpleXmlTransfer.getInstance() + }); return dropTarget; } - /** - * 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<CanvasSelection> mDragSelection = new ArrayList<CanvasSelection>(); - 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} - */ - public void dragStart(DragSourceEvent e) { - // 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. - - mDragSelection.clear(); - - if (!mSelections.isEmpty()) { - // Is the cursor on top of a selected element? - int x = mHScale.inverseTranslate(e.x); - int y = mVScale.inverseTranslate(e.y); - - boolean insideSelection = false; - - for (CanvasSelection cs : mSelections) { - if (!cs.isRoot() && cs.getRect().contains(x, y)) { - insideSelection = true; - break; - } - } - - if (!insideSelection) { - CanvasViewInfo vi = findViewInfoAt(x, y); - if (vi != null) { - 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 (mSelections.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(mSelections); - } else { - // Only drag non-root items. - for (CanvasSelection cs : mSelections) { - if (!cs.isRoot()) { - mDragSelection.add(cs); - } - } - } - } - } - - // If you are dragging a non-selected item, select it - if (mDragSelection.isEmpty()) { - int x = mHScale.inverseTranslate(e.x); - int y = mVScale.inverseTranslate(e.y); - CanvasViewInfo vi = findViewInfoAt(x, y); - if (vi != null) { - selectSingle(vi); - mDragSelection.addAll(mSelections); - } - } - - sanitizeSelection(mDragSelection); - - e.doit = !mDragSelection.isEmpty(); - if (e.doit) { - mDragElements = getSelectionAsElements(mDragSelection); - GlobalCanvasDragInfo.getInstance().startDrag( - mDragElements, - mDragSelection.toArray(new CanvasSelection[mDragSelection.size()]), - LayoutCanvas.this, - new Runnable() { - public void run() { - deleteSelection("Remove", mDragSelection); - } - } - ); - } - } - - /** - * 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} - */ - public void dragSetData(DragSourceEvent e) { - if (TextTransfer.getInstance().isSupportedType(e.dataType)) { - e.data = getSelectionAsText(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. - */ - public void dragFinished(DragSourceEvent e) { - // Clear the selection - mDragSelection.clear(); - mDragElements = null; - GlobalCanvasDragInfo.getInstance().stopDrag(); - } - } - - /** - * 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. - */ - private void sanitizeSelection(List<CanvasSelection> selection) { - if (selection.isEmpty()) { - return; - } - - for (Iterator<CanvasSelection> it = selection.iterator(); it.hasNext(); ) { - CanvasSelection cs = it.next(); - CanvasViewInfo vi = cs.getViewInfo(); - UiViewElementNode key = vi == null ? null : vi.getUiViewKey(); - 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<CanvasSelection> it2 = selection.iterator(); - it2.hasNext(); ) { - CanvasSelection 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; - } - } - } - } - } - } - - /** - * Get the XML text from the given selection for a text transfer. - * The returned string can be empty but not null. - */ - private String getSelectionAsText(List<CanvasSelection> selection) { - StringBuilder sb = new StringBuilder(); - - for (CanvasSelection cs : selection) { - CanvasViewInfo vi = cs.getViewInfo(); - UiViewElementNode key = vi.getUiViewKey(); - Node node = key.getXmlNode(); - String t = getXmlTextFromEditor(mLayoutEditor, node); - if (t != null) { - if (sb.length() > 0) { - sb.append('\n'); - } - sb.append(t); - } - } - - return sb.toString(); - } - - /** - * Get the XML text directly from the editor. - */ - private String getXmlTextFromEditor(AndroidXmlEditor editor, Node xml_node) { - String data = null; - IStructuredModel model = editor.getModelForRead(); - try { - IStructuredDocument sse_doc = editor.getStructuredDocument(); - if (xml_node instanceof NodeContainer) { - // The easy way to get the source of an SSE XML node. - data = ((NodeContainer) xml_node).getSource(); - } else if (xml_node instanceof IndexedRegion && sse_doc != null) { - // Try harder. - IndexedRegion region = (IndexedRegion) xml_node; - int start = region.getStartOffset(); - int end = region.getEndOffset(); - - if (end > start) { - data = sse_doc.get(start, end - start); - } - } - } catch (BadLocationException e) { - // the region offset was invalid. ignore. - } finally { - model.releaseFromRead(); - } - return data; - } - - private SimpleElement[] getSelectionAsElements(List<CanvasSelection> mDragSelection) { - ArrayList<SimpleElement> elements = new ArrayList<SimpleElement>(); - - for (CanvasSelection cs : mDragSelection) { - CanvasViewInfo vi = cs.getViewInfo(); - - SimpleElement e = transformToSimpleElement(vi); - elements.add(e); - } - - return elements.toArray(new SimpleElement[elements.size()]); - } - - private SimpleElement transformToSimpleElement(CanvasViewInfo vi) { - - UiViewElementNode uiNode = vi.getUiViewKey(); - - String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor()); - String parentFqcn = null; - Rect bounds = new Rect(vi.getAbsRect()); - Rect parentBounds = null; - - UiElementNode uiParent = uiNode.getUiParent(); - if (uiParent != null) { - parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor()); - } - if (vi.getParent() != null) { - parentBounds = new Rect(vi.getParent().getAbsRect()); - } - - SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds); - - for (UiAttributeNode attr : uiNode.getUiAttributes()) { - 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 : vi.getChildren()) { - SimpleElement e2 = transformToSimpleElement(childVi); - if (e2 != null) { - e.addInnerElement(e2); - } - } - - return e; - } - //--------------- /** @@ -1934,7 +657,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { mCutAction = new Action() { @Override public void run() { - cutSelectionToClipboard(new ArrayList<CanvasSelection>(mSelections)); + mClipboardSupport.cutSelectionToClipboard(mSelectionManager.getSnapshot()); } }; @@ -1944,7 +667,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { mCopyAction = new Action() { @Override public void run() { - copySelectionToClipboard(new ArrayList<CanvasSelection>(mSelections)); + mClipboardSupport.copySelectionToClipboard(mSelectionManager.getSnapshot()); } }; @@ -1954,7 +677,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { mPasteAction = new Action() { @Override public void run() { - pasteSelection(new ArrayList<CanvasSelection>(mSelections)); + mClipboardSupport.pasteSelection(mSelectionManager.getSnapshot()); } }; @@ -1964,9 +687,9 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { mDeleteAction = new Action() { @Override public void run() { - deleteSelection( - mDeleteAction.getText(), // verb "Delete" from the DELETE action's title - new ArrayList<CanvasSelection>(mSelections)); + mClipboardSupport.deleteSelection( + getDeleteLabel(), + mSelectionManager.getSnapshot()); } }; @@ -1976,7 +699,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { mSelectAllAction = new Action() { @Override public void run() { - onSelectAll(); + mSelectionManager.selectAll(); } }; @@ -1984,11 +707,21 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { copyActionAttributes(mSelectAllAction, ActionFactory.SELECT_ALL); } - /** Update menu actions that depends on the selection. */ - private void updateMenuActions() { + /* package */ String getCutLabel() { + return mCutAction.getText(); + } - boolean hasSelection = !mSelections.isEmpty(); + /* package */ String getDeleteLabel() { + // verb "Delete" from the DELETE action's title + return mDeleteAction.getText(); + } + /** + * Updates menu actions that depends on the selection. + * + * @param hasSelection True iff we have a non-empty selection + */ + /* package */ void updateMenuActions(boolean hasSelection) { mCutAction.setEnabled(hasSelection); mCopyAction.setEnabled(hasSelection); mDeleteAction.setEnabled(hasSelection); @@ -1996,14 +729,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { // 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(); - boolean hasSxt = false; - for (TransferData td : mClipboard.getAvailableTypes()) { - if (sxt.isSupportedType(td)) { - hasSxt = true; - break; - } - } + boolean hasSxt = mClipboardSupport.hasSxtOnClipboard(); mPasteAction.setEnabled(hasSxt); } @@ -2085,7 +811,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { // 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); + MenuManager showInSubMenu = new MenuManager(showInLabel); showInSubMenu.add( ContributionItemFactory.VIEWS_SHOW_IN.create( mLayoutEditor.getSite().getWorkbenchWindow())); @@ -2093,290 +819,26 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { } /** - * Invoked by {@link #mSelectAllAction}. It clears the selection and then - * selects everything (all views and all their children). - */ - private void onSelectAll() { - // First clear the current selection, if any. - mSelections.clear(); - mAltSelection = null; - - // Now select everything if there's a valid layout - if (mIsResultValid && mLastValidViewInfoRoot != null) { - selectAllViewInfos(mLastValidViewInfoRoot); - redraw(); - } - - fireSelectionChanged(); - } - - /** - * Perform the "Copy" action, either from the Edit menu or from the context menu. - * Invoked by {@link #mCopyAction}. - * <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. - */ - private void copySelectionToClipboard(List<CanvasSelection> selection) { - sanitizeSelection(selection); - - if (selection.isEmpty()) { - return; - } - - Object[] data = new Object[] { - getSelectionAsElements(selection), - getSelectionAsText(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. - * Invoked by {@link #mCutAction}. - * <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. + * Deletes the selection. Equivalent to pressing the Delete key. */ - private void cutSelectionToClipboard(List<CanvasSelection> selection) { - copySelectionToClipboard(selection); - deleteSelection( - mCutAction.getText(), // verb "Cut" from the CUT action's title - selection); - } - - /** - * Deletes the given selection. - * <p/> - * This can either be invoked directly by {@link #mDeleteAction}, or as - * an implementation detail as part of {@link #mCutAction} or also when removing - * the elements after a successful "MOVE" drag'n'drop. - * - * @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 - * {@link #mCutAction} or {@link #mDeleteAction}. - * @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 give a copy of - * {@link #mSelections}, directly or indirectly. - */ - private void deleteSelection(String verb, final List<CanvasSelection> selection) { - sanitizeSelection(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 (CanvasSelection cs : selection) { - CanvasViewInfo vi = cs.getViewInfo(); - if (vi != null && vi.getParent() != null) { - if (title == null) { - title = vi.getParent().getName(); - } else if (!title.equals(vi.getParent().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 = mSelections.size() > 1; - 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. - mLayoutEditor.wrapUndoEditXmlModel(title, new Runnable() { - public void run() { - for (CanvasSelection cs : selection) { - CanvasViewInfo vi = cs.getViewInfo(); - if (vi != null) { - UiViewElementNode ui = vi.getUiViewKey(); - if (ui != null) { - ui.deleteXmlNode(); - } - } - } - } - }); - } - - /** - * Perform the "Paste" action, either from the Edit menu or from the context menu. - */ - private void pasteSelection(List<CanvasSelection> selection) { - - SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance(); - SimpleElement[] pasted = (SimpleElement[]) mClipboard.getContents(sxt); - - if (pasted == null || pasted.length == 0) { - return; - } - - if (mLastValidViewInfoRoot == 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. - sanitizeSelection(selection); - CanvasViewInfo target = mLastValidViewInfoRoot; - if (selection.size() > 0) { - CanvasSelection cs = selection.get(0); - target = cs.getViewInfo(); - } - - NodeProxy targetNode = mNodeFactory.create(target); - - getRulesEngine().callOnPaste(targetNode, pasted); - } - - /** - * 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(String)} 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 UiDocumentNode uiDoc = mLayoutEditor.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 = mLayoutEditor.getFqcnViewDescritor(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); - - mLayoutEditor.wrapUndoEditXmlModel(title, new Runnable() { - public void run() { - UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc); - - // A root node requires the Android XMLNS - uiNew.setAttributeValue( - "android", - XmlnsAttributeDescriptor.XMLNS_URI, - SdkConstants.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 = - mLayoutEditor.getFqcnViewDescritor(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); - } - } - }); + /* package */ 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. + * 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 CanvasDropListener#drop(org.eclipse.swt.dnd.DropTargetEvent)}. + * This is invoked by + * {@link MoveGesture#drop(org.eclipse.swt.dnd.DropTargetEvent)}. * - * @param rootFqcn A non-null non-empty FQCN that must match an existing {@link ViewDescriptor} - * to add as root to the current empty XML document. + * @param rootFqcn A non-null non-empty FQCN that must match an existing + * {@link ViewElementDescriptor} to add as root to the current + * empty XML document. */ /* package */ void createDocumentRoot(String rootFqcn) { @@ -2388,7 +850,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { } // Find the view descriptor matching our FQCN - final ViewElementDescriptor viewDesc = mLayoutEditor.getFqcnViewDescritor(rootFqcn); + final ViewElementDescriptor viewDesc = mLayoutEditor.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); @@ -2423,7 +885,9 @@ class LayoutCanvas extends Canvas implements ISelectionProvider { } private void debugPrintf(String message, Object... params) { - if (DEBUG) AdtPlugin.printToConsole("Canvas", String.format(message, params)); + if (DEBUG) { + AdtPlugin.printToConsole("Canvas", String.format(message, params)); + } } } 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 index f509c19..f1b4247 100755 --- 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 @@ -52,13 +52,15 @@ class LayoutCanvasViewer extends Viewer { mLayoutEditor = layoutEditor; mCanvas = new LayoutCanvas(layoutEditor, rulesEngine, parent, style); - mCanvas.addSelectionChangedListener(new ISelectionChangedListener() { - public void selectionChanged(SelectionChangedEvent event) { - fireSelectionChanged(event); - } - }); + mCanvas.getSelectionManager().addSelectionChangedListener(mSelectionListener); } + private ISelectionChangedListener mSelectionListener = new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + fireSelectionChanged(event); + } + }; + @Override public Control getControl() { return mCanvas; @@ -70,6 +72,7 @@ class LayoutCanvasViewer extends Viewer { * have it already casted in the right type. * <p/> * This can never be null. + * @return The underlying {@link LayoutCanvas}. */ public LayoutCanvas getCanvas() { return mCanvas; @@ -96,7 +99,7 @@ class LayoutCanvasViewer extends Viewer { */ @Override public ISelection getSelection() { - return mCanvas.getSelection(); + return mCanvas.getSelectionManager().getSelection(); } /** @@ -106,7 +109,7 @@ class LayoutCanvasViewer extends Viewer { */ @Override public void setSelection(ISelection selection, boolean reveal) { - mCanvas.setSelection(selection); + mCanvas.getSelectionManager().setSelection(selection); } /** Unused. Refreshing is done solely by the owning {@link LayoutEditor}. */ @@ -120,5 +123,8 @@ class LayoutCanvasViewer extends Viewer { mCanvas.dispose(); mCanvas = null; } + if (mSelectionListener != null) { + mCanvas.getSelectionManager().removeSelectionChangedListener(mSelectionListener); + } } } 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 0000000..77ea8da --- /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,145 @@ +/* + * 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.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) { + this.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); + } + + @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/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 0000000..f9c91f4 --- /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,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 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) { + this.mCanvas = canvas; + + if (toggle) { + List<CanvasSelection> selection = canvas.getSelectionManager().getSelections(); + mInitialSelection = new ArrayList<CanvasViewInfo>(selection.size()); + for (CanvasSelection item : selection) { + mInitialSelection.add(item.getViewInfo()); + } + } else { + mInitialSelection = Collections.emptySet(); + } + } + + @Override + public void update(ControlPoint pos) { + 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 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/CanvasDropListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java index f8db888..7302aae 100755..100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasDropListener.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 The Android Open Source Project + * 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. @@ -13,7 +13,6 @@ * 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; @@ -22,30 +21,33 @@ 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.NodeProxy; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; import org.eclipse.swt.dnd.DND; import org.eclipse.swt.dnd.DropTargetEvent; -import org.eclipse.swt.dnd.DropTargetListener; 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.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** - * Handles drop operations on top of the canvas. - * <p/> - * Reference for d'n'd: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html + * The Move gesture provides the operation for moving widgets around in the canvas. */ -/* package */ class CanvasDropListener implements DropTargetListener { +public class MoveGesture extends DropGesture { + /** The associated {@link LayoutCanvas}. */ + private LayoutCanvas mCanvas; - private static final boolean DEBUG = false; + /** Overlay which paints the drag & drop feedback. */ + private MoveOverlay mOverlay; - private final LayoutCanvas mCanvas; + private static final boolean DEBUG = false; /** * The top view right under the drag'n'drop cursor. @@ -88,10 +90,12 @@ import java.util.Set; * happens next. */ private NodeProxy mLeaveTargetNode; + /** * @see #mLeaveTargetNode */ private DropFeedback mLeaveFeedback; + /** * @see #mLeaveTargetNode */ @@ -100,15 +104,27 @@ import java.util.Set; /** Singleton used to keep track of drag selection in the same Eclipse instance. */ private final GlobalCanvasDragInfo mGlobalDragInfo; - public CanvasDropListener(LayoutCanvas canvas) { - mCanvas = canvas; + /** + * Constructs a new {@link MoveGesture}, tied to the given canvas. + * + * @param canvas The canvas to associate the {@link MoveGesture} with. + */ + public MoveGesture(LayoutCanvas canvas) { + this.mCanvas = canvas; mGlobalDragInfo = GlobalCanvasDragInfo.getInstance(); } + @Override + public List<Overlay> createOverlays() { + mOverlay = new MoveOverlay(); + return Collections.<Overlay> singletonList(mOverlay); + } + /* * The cursor has entered the drop target boundaries. * {@inheritDoc} */ + @Override public void dragEnter(DropTargetEvent event) { if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag enter", event); @@ -156,6 +172,7 @@ import java.util.Set; * 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); @@ -187,6 +204,7 @@ import java.util.Set; * 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"); @@ -206,6 +224,7 @@ import java.util.Set; * The cursor is moving over the drop target. * {@inheritDoc} */ + @Override public void dragOver(DropTargetEvent event) { processDropEvent(event); } @@ -215,6 +234,7 @@ import java.util.Set; * 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"); @@ -246,6 +266,7 @@ import java.util.Set; * The data is being dropped. * {@inheritDoc} */ + @Override public void drop(final DropTargetEvent event) { if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "dropped"); @@ -269,7 +290,8 @@ import java.util.Set; } if (mTargetNode == null) { - if (mCanvas.isResultValid() && mCanvas.isEmptyDocument()) { + 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); @@ -279,7 +301,7 @@ import java.util.Set; return; } - final Point where = mCanvas.displayToCanvasPoint(event.x, event.y); + final LayoutPoint canvasPoint = ControlPoint.create(mCanvas, event).toLayout(); // Record children of the target right before the drop (such that we can // find out after the drop which exact children were inserted) @@ -297,7 +319,7 @@ import java.util.Set; mCanvas.getRulesEngine().callOnDropped(mTargetNode, elementsFinal, mFeedback, - where); + new Point(canvasPoint.x, canvasPoint.y)); // Clean up drag if applicable if (event.detail == DND.DROP_MOVE) { GlobalCanvasDragInfo.getInstance().removeSource(); @@ -345,19 +367,19 @@ import java.util.Set; private boolean selectDropped(Collection<INode> nodes) { final Collection<CanvasViewInfo> newChildren = new ArrayList<CanvasViewInfo>(); for (INode node : nodes) { - CanvasViewInfo viewInfo = mCanvas.findViewInfoFor(node); + CanvasViewInfo viewInfo = mCanvas.getViewHierarchy().findViewInfoFor(node); if (viewInfo != null) { newChildren.add(viewInfo); } } - mCanvas.selectMultiple(newChildren); + mCanvas.getSelectionManager().selectMultiple(newChildren); return nodes.size() == newChildren.size(); } /** * Computes a suitable Undo label to use for a drop operation, such as - * "Drop Button in LinearLayout" and "Move Widgets in RelativeLayout" + * "Drop Button in LinearLayout" and "Move Widgets in RelativeLayout". * * @param targetNode The target of the drop * @param elements The dragged widgets @@ -390,13 +412,13 @@ import java.util.Set; * Returns simple name (basename, following last dot) of a fully qualified * class name. * - * @param fcqn The fqcn to reduce + * @param fqcn The fqcn to reduce * @return The base name of the fqcn */ private 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) = fcqn + // fcqn.substring(0) = fqcn return fqcn.substring(fqcn.lastIndexOf('.') + 1); } @@ -417,17 +439,6 @@ import java.util.Set; } /** - * Invoked by the canvas to refresh the display. - * @param gCWrapper The GC wrapper, never null. - */ - public void paintFeedback(GCWrapper gCWrapper) { - if (mTargetNode != null && mFeedback != null && mFeedback.requestPaint) { - mCanvas.getRulesEngine().callDropFeedbackPaint(gCWrapper, mTargetNode, mFeedback); - mFeedback.requestPaint = false; - } - } - - /** * 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. @@ -466,7 +477,7 @@ import java.util.Set; * selected target node. */ private void processDropEvent(DropTargetEvent event) { - if (!mCanvas.isResultValid()) { + 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; @@ -474,15 +485,13 @@ import java.util.Set; return; } - Point p = mCanvas.displayToCanvasPoint(event.x, event.y); - int x = p.x; - int y = p.y; + LayoutPoint p = ControlPoint.create(mCanvas, 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(x, y); + isCaptured = r != null && r.contains(p.x, p.y); } // We can't switch views/nodes when the mouse is captured @@ -490,7 +499,7 @@ import java.util.Set; if (isCaptured) { vi = mCurrentView; } else { - vi = mCanvas.findViewInfoAt(x, y); + vi = mCanvas.getViewHierarchy().findViewInfoAt(p); } boolean isMove = true; @@ -532,7 +541,7 @@ import java.util.Set; // the -position- of the mouse), and we want this computation to happen // before we ask the view to draw its feedback. df = mCanvas.getRulesEngine().callOnDropMove(targetNode, - mCurrentDragElements, df, p); + mCurrentDragElements, df, new Point(p.x, p.y)); } if (df != null && @@ -587,7 +596,7 @@ import java.util.Set; 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(x, y); + new com.android.ide.common.api.Point(p.x, p.y); updateDropFeedback(mFeedback, event); DropFeedback df = mCanvas.getRulesEngine().callOnDropMove( mTargetNode, mCurrentDragElements, mFeedback, p2); @@ -655,4 +664,19 @@ import java.util.Set; mCanvas.createDocumentRoot(rootFqcn); } + + /** + * 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/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 0000000..942da2b --- /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,104 @@ +/* + * 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 ScaleInfo mVScale; + + /** Horizontal scaling & scrollbar information. */ + private ScaleInfo mHScale; + + /** + * Constructs a new {@link OutlineOverlay} linked to the given view + * hierarchy. + * + * @param viewHierarchy The {@link ViewHierarchy} to render + * @param hScale The {@link ScaleInfo} to use to transfer horizontal layout + * coordinates to screen coordinates + * @param vScale The {@link ScaleInfo} to use to transfer vertical layout + * coordinates to screen coordinates + */ + public OutlineOverlay(ViewHierarchy viewHierarchy, ScaleInfo hScale, ScaleInfo vScale) { + super(); + this.mViewHierarchy = viewHierarchy; + this.mHScale = hScale; + this.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/OutlinePage2.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage2.java index fdbd8fd..0a267d6 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage2.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage2.java @@ -155,8 +155,11 @@ public class OutlinePage2 extends ContentOutlinePage } }); - mDragSource = LayoutCanvas.createDragSource(getControl(), new DelegateDragListener()); - mDropTarget = LayoutCanvas.createDropTarget(getControl(), new DelegateDropListener()); + mDragSource = LayoutCanvas.createDragSource(getControl()); + mDragSource.addDragListener(new DelegateDragListener()); + + mDropTarget = LayoutCanvas.createDropTarget(getControl()); + mDropTarget.addDropListener(new DelegateDropListener()); setupContextMenu(); @@ -716,7 +719,7 @@ public class OutlinePage2 extends ContentOutlinePage LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl(); if (canvas != null) { com.android.ide.common.api.Point p = - canvas.canvasToControlPoint(x, y); + canvas.layoutToControlPoint(x, y); inOutXY.x = p.x; inOutXY.y = p.y; 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 0000000..ac96d76 --- /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,62 @@ +/* + * 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 { + /** + * 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) { + } + + /** + * 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"); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ScaleInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ScaleInfo.java new file mode 100644 index 0000000..0565549 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ScaleInfo.java @@ -0,0 +1,143 @@ +/* + * 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 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 ScaleInfo implements ICanvasTransform { + /** + * The canvas which controls the zooming. + */ + private final LayoutCanvas mCanvas; + + /** Canvas image size (original, before zoom), in pixels. */ + private int mImgSize; + + /** Client size, in pixels. */ + private int mClientSize; + + /** Left-top offset in client pixel coordinates. */ + private int mTranslate; + + /** Scaling factor, > 0. */ + private double mScale; + + /** Scrollbar widget. */ + private ScrollBar mScrollbar; + + public ScaleInfo(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(); + ScaleInfo.this.mCanvas.redraw(); + } + }); + } + + /** + * Sets the new scaling factor. Recomputes scrollbars. + * @param scale Scaling factor, > 0. + */ + public void setScale(double scale) { + if (mScale != scale) { + mScale = scale; + 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 getScalledImgSize() { + return (int) (mImgSize * mScale); + } + + /** Changes the size of the canvas image and the client size. Recomputes scrollbars. */ + public void setSize(int imgSize, int clientSize) { + mImgSize = imgSize; + setClientSize(clientSize); + } + + /** Changes the size of the client size. Recomputes scrollbars. */ + public void setClientSize(int clientSize) { + mClientSize = clientSize; + resizeScrollbar(); + } + + private void resizeScrollbar() { + // scaled image size + int sx = (int) (mImgSize * mScale); + + // actual client area is always reduced by the margins + int cx = mClientSize - 2 * IMAGE_MARGIN; + + if (sx < cx) { + mScrollbar.setEnabled(false); + } else { + mScrollbar.setEnabled(true); + + // max scroll value is the scaled image size + // thumb value is the actual viewable area out of the scaled img size + mScrollbar.setMaximum(sx); + mScrollbar.setThumb(cx); + } + } + + public int translate(int canvasX) { + return IMAGE_MARGIN - mTranslate + (int) (mScale * canvasX); + } + + public int scale(int canwasW) { + return (int) (mScale * canwasW); + } + + public int inverseTranslate(int screenX) { + return (int) ((screenX - IMAGE_MARGIN + mTranslate) / mScale); + } +} 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 0000000..affd69a --- /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,623 @@ +/* + * 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.uimodel.UiViewElementNode; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.runtime.ListenerList; +import org.eclipse.gef.ui.parts.TreeViewer; +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.swt.SWT; +import org.eclipse.swt.events.MouseEvent; +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<CanvasSelection> mSelections = new LinkedList<CanvasSelection>(); + + /** An unmodifiable view of {@link #mSelections}. */ + private final List<CanvasSelection> 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) { + this.mCanvas = layoutCanvas; + } + + public void addSelectionChangedListener(ISelectionChangedListener listener) { + mSelectionListeners.add(listener); + } + + public void removeSelectionChangedListener(ISelectionChangedListener listener) { + mSelectionListeners.remove(listener); + } + + /** + * Returns the native {@link CanvasSelection} list. + * + * @return An immutable list of {@link CanvasSelection}. Can be empty but not null. + * @see #getSelection() {@link #getSelection()} to retrieve a {@link TreeViewer} + * compatible {@link ISelection}. + */ + /* package */ List<CanvasSelection> 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. + */ + /* package */ List<CanvasSelection> getSnapshot() { + return new ArrayList<CanvasSelection>(mSelections); + } + + /** + * Returns a {@link TreeSelection} compatible with a TreeViewer + * where each {@link TreePath} item is actually a {@link CanvasViewInfo}. + */ + public ISelection getSelection() { + if (mSelections.isEmpty()) { + return TreeSelection.EMPTY; + } + + ArrayList<TreePath> paths = new ArrayList<TreePath>(); + + for (CanvasSelection cs : mSelections) { + CanvasViewInfo vi = cs.getViewInfo(); + if (vi != null) { + ArrayList<Object> segments = new ArrayList<Object>(); + while (vi != null) { + segments.add(0, vi); + vi = vi.getParent(); + } + paths.add(new TreePath(segments.toArray())); + } + } + + return new TreeSelection(paths.toArray(new TreePath[paths.size()])); + } + + /** + * 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. + */ + public void setSelection(ISelection selection) { + if (mInsideUpdateSelection) { + return; + } + + 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; + redraw(); + } + return; + } + + boolean changed = false; + + // Create a list of all currently selected view infos + Set<CanvasViewInfo> oldSelected = new HashSet<CanvasViewInfo>(); + for (CanvasSelection 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; + } + } + } + + // Deselect old selected items that are not in the new one + for (CanvasViewInfo vi : oldSelected) { + deselect(vi); + changed = true; + } + + if (changed) { + redraw(); + updateMenuActions(); + } + + } + } finally { + mInsideUpdateSelection = false; + } + } + + /** + * 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 (CanvasSelection 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 (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)) { + 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 {@link #redraw()} if the selection changes. + * + * @param vi The new selected item if non-null. Selection becomes empty if null. + */ + /* package */ void selectSingle(CanvasViewInfo vi) { + // reset alternate selection if any + mAltSelection = null; + + // 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.clear(); + } + + if (vi != null) { + mSelections.add(createSelection(vi)); + } + fireSelectionChanged(); + redraw(); + } + + /** + * 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; + + mSelections.clear(); + if (viewInfos != null) { + for (CanvasViewInfo viewInfo : viewInfos) { + mSelections.add(createSelection(viewInfo)); + } + } + + fireSelectionChanged(); + redraw(); + } + + /** + * 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) { + 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) { + 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(); + } + + /** + * 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<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) { + CanvasSelection 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<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) { + CanvasSelection s = it.next(); + if (canvasViewInfos.contains(s.getViewInfo())) { + it.remove(); + } + } + } + + /** Sync the selection with an updated view info tree */ + /* package */ void sync(CanvasViewInfo lastValidViewInfoRoot) { + // Check if the selection is still the same (based on the object keys) + // and eventually recompute their bounds. + for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) { + CanvasSelection s = it.next(); + + // Check if the selected object still exists + ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy(); + Object key = s.getViewInfo().getUiViewKey(); + CanvasViewInfo vi = viewHierarchy.findViewInfoKey(key, lastValidViewInfoRoot); + + // 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) { + it.add(createSelection(vi)); + } + } + fireSelectionChanged(); + + // remove the current alternate selection views + mAltSelection = 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() { + public void run() { + for (Object listener : mSelectionListeners.getListeners()) { + ((ISelectionChangedListener) listener).selectionChanged(event); + } + } + }); + + // Update menu actions that depend on the selection + updateMenuActions(); + + } finally { + mInsideUpdateSelection = false; + } + } + + /** + * 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<CanvasSelection> selection) { + if (selection.isEmpty()) { + return; + } + + for (Iterator<CanvasSelection> it = selection.iterator(); it.hasNext(); ) { + CanvasSelection cs = it.next(); + CanvasViewInfo vi = cs.getViewInfo(); + UiViewElementNode key = vi == null ? null : vi.getUiViewKey(); + 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<CanvasSelection> it2 = selection.iterator(); + it2.hasNext(); ) { + CanvasSelection 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; + } + } + } + } + } + } + + private void updateMenuActions() { + boolean hasSelection = !mSelections.isEmpty(); + mCanvas.updateMenuActions(hasSelection); + } + + private void redraw() { + mCanvas.redraw(); + } + + private CanvasSelection createSelection(CanvasViewInfo vi) { + return new CanvasSelection(vi, mCanvas.getRulesEngine(), + mCanvas.getNodeFactory()); + } +} 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 0000000..05e405a --- /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,85 @@ +/* + * 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.gre.RulesEngine; + +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Device; +import org.eclipse.swt.graphics.GC; + +import java.util.List; + +/** + * The {@link SelectionOverlay} paints the current selection as an overlay. + */ +public class SelectionOverlay extends Overlay { + /** CanvasSelection border color. */ + private Color mSelectionFgColor; + + /** + * Constructs a new {@link SelectionOverlay} tied to the given canvas. + */ + public SelectionOverlay() { + } + + @Override + public void create(Device device) { + mSelectionFgColor = new Color(device, SwtDrawingStyle.SELECTION.getStrokeColor()); + } + + @Override + public void dispose() { + if (mSelectionFgColor != null) { + mSelectionFgColor.dispose(); + mSelectionFgColor = null; + } + } + + /** + * Paints the selection. + * + * @param selectionManager The {@link SelectionManager} holding the + * selection. + * @param gc The graphics context to draw into. + * @param gcWrapper The graphics context wrapper for the layout rules to use. + * @param rulesEngine The {@link RulesEngine} holding the rules. + */ + public void paint(SelectionManager selectionManager, GC gc, GCWrapper gcWrapper, + RulesEngine rulesEngine) { + List<CanvasSelection> selections = selectionManager.getSelections(); + int n = selections.size(); + if (n > 0) { + boolean isMultipleSelection = n > 1; + + if (n == 1) { + gc.setForeground(mSelectionFgColor); + selections.get(0).paintParentSelection(rulesEngine, gcWrapper); + } + + for (CanvasSelection s : selections) { + if (s.isRoot()) { + // The root selection is never painted + continue; + } + gc.setForeground(mSelectionFgColor); + s.paintSelection(rulesEngine, gcWrapper, isMultipleSelection); + } + } + } + +} 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 index 8c298e1..11f3ea2 100755 --- 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 @@ -59,7 +59,7 @@ import java.io.UnsupportedEncodingException; * {@link SimpleAttribute}s, all of which have very specific properties that are * specifically limited to our needs for drag'n'drop. */ -class SimpleXmlTransfer extends ByteArrayTransfer { +final class SimpleXmlTransfer extends ByteArrayTransfer { // Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html 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 index db8dfcf..36fdad9 100644 --- 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 @@ -34,7 +34,7 @@ public enum SwtDrawingStyle { /** * The style definition corresponding to {@link DrawingStyle#SELECTION} */ - SELECTION(new RGB(0x00, 0x99, 0xFF), 255, new RGB(0x00, 0x99, 0xFF), 64, 2, SWT.LINE_DASH), + SELECTION(new RGB(0x00, 0x99, 0xFF), 255, new RGB(0x00, 0x99, 0xFF), 64, 1, SWT.LINE_DASH), /** * The style definition corresponding to {@link DrawingStyle#GUIDELINE} 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 0000000..24d0caa --- /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,423 @@ +/* + * 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.eclipse.adt.internal.editors.layout.gre.NodeProxy; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; +import com.android.layoutlib.api.ILayoutResult; +import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo; + +import org.eclipse.swt.graphics.Rectangle; +import org.w3c.dom.Node; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * The view hierarchy class manages a set of view info objects and performs find + * operations on this set. + */ +public class ViewHierarchy { + 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) { + this.mCanvas = canvas; + } + + /** + * The CanvasViewInfo root created by the last call to {@link #setResult(ILayoutResult)} + * 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 #setResult(ILayoutResult)} provided a valid {@link ILayoutResult}. + * <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; + + /** + * 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 result The new rendering result, either valid or not. + */ + /* package */ void setResult(ILayoutResult result) { + mIsResultValid = (result != null && result.getSuccess() == ILayoutResult.SUCCESS); + + if (mIsResultValid && result != null) { + ILayoutViewInfo root = result.getRootView(); + if (root == null) { + mLastValidViewInfoRoot = null; + } else { + mLastValidViewInfoRoot = new CanvasViewInfo(result.getRootView()); + } + + updateNodeProxies(mLastValidViewInfoRoot); + + // Update the selection + mCanvas.getSelectionManager().sync(mLastValidViewInfoRoot); + } + } + + /** + * 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.getUiViewKey(); + + if (key != null) { + mCanvas.getNodeFactory().create(vi); + } + + for (CanvasViewInfo child : vi.getChildren()) { + updateNodeProxies(child); + } + } + + + + /** + * Returns true when the last {@link #setResult(ILayoutResult)} provided a valid + * {@link ILayoutResult}. + * <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; + } + + /** 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. + */ + public CanvasViewInfo findViewInfoFor(Node node) { + if (mLastValidViewInfoRoot != null) { + return findViewInfoForNode(node, mLastValidViewInfoRoot); + } + return null; + } + + /** + * Tries to find a child with the same view XML node in the view info sub-tree. + * Returns null if not found. + */ + private CanvasViewInfo findViewInfoForNode(Node xmlNode, CanvasViewInfo canvasViewInfo) { + if (canvasViewInfo == null) { + return null; + } + if (canvasViewInfo.getXmlNode() == xmlNode) { + return canvasViewInfo; + } + + // Try to find a matching child + for (CanvasViewInfo child : canvasViewInfo.getChildren()) { + CanvasViewInfo v = findViewInfoForNode(xmlNode, child); + if (v != null) { + return v; + } + } + + return null; + } + + + /** + * 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 + for (CanvasViewInfo child : canvasViewInfo.getChildren()) { + 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) { + if (mLastValidViewInfoRoot != null && node instanceof NodeProxy) { + return findViewInfoKey(((NodeProxy) node).getNode(), mLastValidViewInfoRoot); + } + return null; + } + + /** + * 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. + * @param canvasViewInfo A root {@link CanvasViewInfo} to search from. + * @return A {@link CanvasViewInfo} matching the given key, or null if not + * found. + */ + public CanvasViewInfo findViewInfoKey(Object viewKey, CanvasViewInfo canvasViewInfo) { + if (canvasViewInfo == null) { + return null; + } + if (canvasViewInfo.getUiViewKey() == viewKey) { + return canvasViewInfo; + } + + // try to find a matching child + for (CanvasViewInfo child : canvasViewInfo.getChildren()) { + CanvasViewInfo v = findViewInfoKey(viewKey, child); + if (v != null) { + return v; + } + } + + return null; + } + + /** + * 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); + } + } + } + + /** + * Return 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; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java index c6c84a7..530be06 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java @@ -367,7 +367,7 @@ public class NodeProxy implements INode { private ViewElementDescriptor getFqcnViewDescritor(String fqcn) { AndroidXmlEditor editor = mNode.getEditor(); if (editor instanceof LayoutEditor) { - return ((LayoutEditor) editor).getFqcnViewDescritor(fqcn); + return ((LayoutEditor) editor).getFqcnViewDescriptor(fqcn); } return null; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java index dc8b79c..1851fa7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java @@ -92,7 +92,7 @@ public class UiViewElementNode extends UiElementNode { } } } - } else if (ui_parent instanceof UiViewElementNode){ + } else if (ui_parent instanceof UiViewElementNode) { layout_attrs = ((ViewElementDescriptor) ui_parent.getDescriptor()).getLayoutAttributes(); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/InstrumentationRunnerValidator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/InstrumentationRunnerValidator.java index 5c88cf2..bb75bcc 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/InstrumentationRunnerValidator.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/InstrumentationRunnerValidator.java @@ -98,7 +98,7 @@ class InstrumentationRunnerValidator { /** * Return the set of instrumentation names for the Android project. * - * @return <code>null</code if error occurred parsing instrumentations, otherwise returns array + * @return <code>null</code> if error occurred parsing instrumentations, otherwise returns array * of instrumentation class names */ String[] getInstrumentationNames() { |