diff options
author | Raphael Moll <ralf@android.com> | 2010-06-21 13:09:23 -0700 |
---|---|---|
committer | Android Code Review <code-review@android.com> | 2010-06-21 13:09:23 -0700 |
commit | 13b97d8b6a4137d9be28e60196eb6d44dee742fa (patch) | |
tree | 426093426c1b67c51fd67a16a2f440c12f20cc3d | |
parent | e48a35070a0b27ec8ea99e2cd3bf999a11b175b5 (diff) | |
parent | 8c7e29f5cb9f1200959f3beb5aa03eacf210b004 (diff) | |
download | sdk-13b97d8b6a4137d9be28e60196eb6d44dee742fa.zip sdk-13b97d8b6a4137d9be28e60196eb6d44dee742fa.tar.gz sdk-13b97d8b6a4137d9be28e60196eb6d44dee742fa.tar.bz2 |
Merge changes Ib9f3473d,Ic9ac3d34,I1e26f9c5,I64959e27
* changes:
ADT GLE2: cleanup some constants and review feedback.
ADT GLE2: drag'n'drop in RelativeLayout.
ADT GLE2: readme (first quick'n'dirty draft)
ADT GLE2: drag'n'drop for LinearLayout
20 files changed, 949 insertions, 355 deletions
diff --git a/docs/gscripts.txt b/docs/gscripts.txt new file mode 100755 index 0000000..e3f5d18 --- /dev/null +++ b/docs/gscripts.txt @@ -0,0 +1,49 @@ +This file describes the "gscripts" folder in ADT (Android Eclipse Plugin). + + +---------- +- Overview +---------- + +ADT is the Android Eclipse Plugin. The plugin delivers a new editor, called +the the Graphical Layout Editor (a.k.a. GLE2), to visually edit Android layout +XML files. + +Details on how to handle the various Android views and layouts is not +hardcoded in the GLE2 itself. Instead it is differed to a bunch of Groovy +scripts. + + +(TODO: expand/replace with a better overview of implementation... goal is +to use this a doc for 3rd-party projects to implement their own rules.) + + + +------------- +- Groovy tips +------------- + + +- Debugging: + +If you run ADT in debug mode and want to trace into Groovy +methods, you need to tell Eclipse where to find the Groovy source code. + +To do this: +- in Eclipse, import an existing project +- Select the project at <android-source-tree>/prebuilt/common/groovy/ +- This will add a new Eclipse project named "GroovySrc" which contains + a single zip file with the groovy source. +- ADT is already pre-configured to find the Groovy source in the GroovySrc + project. + + + +- Private methods: + +Be careful when adding new helper methods in the BaseView +or BaseLayout classes. + +Due to the way Groovy looks up methods, private methods will *not* be found by +same-class methods if invoked by a derived class in the context of a closure +(which is about the case of all these helper methods.) diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseLayout.groovy b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseLayout.groovy index 422dc70..98e0b0f 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseLayout.groovy +++ b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseLayout.groovy @@ -26,4 +26,258 @@ public class BaseLayout extends BaseView { super.onDispose(); } + // ==== Utility methods used by derived layouts ==== + + // TODO revisit. + protected String[] getLayoutAttrFilter() { + return [ + // from AbsoluteLayout + "layout_x", + "layout_y", + + // from RelativeLayout + "layout_above", + "layout_below", + "layout_toLeftOf", + "layout_toRightOf", + "layout_alignBaseline", + "layout_alignTop", + "layout_alignBottom", + "layout_alignLeft", + "layout_alignRight", + "layout_alignParentTop", + "layout_alignParentBottom", + "layout_alignParentLeft", + "layout_alignParentRight", + "layout_alignWithParentMissing", + "layout_centerHorizontal", + "layout_centerInParent", + "layout_centerVertical", + ]; + } + + /** + * Draws the bounds of the given elements and all its children elements + * in the canvas with the specified offet. + */ + protected void drawElement(IGraphics gc, IDragElement element, int offsetX, int offsetY) { + Rect b = element.getBounds(); + if (b.isValid()) { + b = b.copy().offsetBy(offsetX, offsetY); + gc.drawRect(b); + } + + for(inner in element.getInnerElements()) { + drawElement(gc, inner, offsetX, offsetY); + } + } + + /** + * Collect all the "android:id" IDs from the dropped elements. + * + * When moving objects within the same canvas, that's all there is to do. + * However if the objects are moved to a different canvas or are copied + * then set createNewIds to true to find the existing IDs under targetNode + * and create a map with new non-conflicting unique IDs as needed. + * + * Returns a map String old-id => tuple (String new-id, String fqcn) + * where fqcn is the FQCN of the element. + */ + protected Map getDropIdMap(INode targetNode, + IDragElement[] elements, + boolean createNewIds) { + def idMap = [:]; + + if (createNewIds) { + collectIds(idMap, elements); + // Need to remap ids if necessary + idMap = remapIds(targetNode, idMap); + } + + return idMap; + } + + + /** + * Fills idMap with a map String id => tuple (String id, String fqcn) + * where fqcn is the FQCN of the element (in case we want to generate + * new IDs based on the element type.) + * + * @see #getDropIdMap + */ + protected Map collectIds(Map idMap, IDragElement[] elements) { + for (element in elements) { + def attr = element.getAttribute(ANDROID_URI, ATTR_ID); + if (attr != null) { + String id = attr.getValue(); + if (id != null && id != "") { + idMap.put(id, [id, element.getFqcn()]); + } + } + + collectIds(idMap, element.getInnerElements()); + } + + return idMap; + } + + /** + * Used by #getDropIdMap to find new IDs in case of conflict. + */ + protected Map remapIds(INode node, Map idMap) { + // Visit the document to get a list of existing ids + def existingIdMap = [:]; + collectExistingIds(node.getRoot(), existingIdMap); + + def new_map = [:]; + idMap.each() { key, value -> + def id = normalizeId(key); + + if (!existingIdMap.containsKey(id)) { + // Not a conflict. Use as-is. + new_map.put(key, value); + if (key != id) { + new_map.put(id, value); + } + } else { + // There is a conflict. Get a new id. + def new_id = findNewId(value[1], existingIdMap); + value[0] = new_id; + new_map.put(id, value); + new_map.put(id.replaceFirst("@\\+", "@"), value); + } + } + + return new_map; + } + + /** + * Used by #remapIds to find a new ID for a conflicting element. + */ + protected String findNewId(String fqcn, Map existingIdMap) { + // Get the last component of the FQCN (e.g. "android.view.Button" => "Button") + String name = fqcn[fqcn.lastIndexOf(".")+1 .. fqcn.length()-1]; + + for (int i = 1; i < 1000000; i++) { + String id = String.format("@+id/%s%02d", name, i); + if (!existingIdMap.containsKey(id)) { + existingIdMap.put(id, id); + return id; + } + } + + // We'll never reach here. + return null; + } + + /** + * Used by #getDropIdMap to find existing IDs recursively. + */ + protected void collectExistingIds(INode root, Map existingIdMap) { + if (root == null) { + return; + } + + def id = root.getStringAttr(ANDROID_URI, ATTR_ID); + if (id != null) { + id = normalizeId(id); + + if (!existingIdMap.containsKey(id)) { + existingIdMap.put(id, id); + } + } + + for(child in root.getChildren()) { + collectExistingIds(child, existingIdMap); + } + } + + /** + * Transforms @id/name into @+id/name to treat both forms the same way. + */ + protected String normalizeId(String id) { + if (id.indexOf("@+") == -1) { + id = id.replaceFirst("@", "@+"); + } + return id; + } + + /** + * Copies all the attributes from oldElement to newNode. + * + * Uses the idMap to transform the value of all attributes of Format.REFERENCE, + * If filter is non-null, it's a closure that takes for argument: + * String attribue-uri (namespace), String attribute-name, String attribute-value + * The closure should return a valid replacement string. + * The closure can return either null, false or an empty string to prevent the attribute + * from being copied into the new node. + */ + protected void addAttributes(INode newNode, IDragElement oldElement, + Map idMap, Closure filter) { + + // A little trick here: when creating new UI widgets by dropping them from + // the palette, we assign them a new id and then set the text attribute + // to that id, so for example a Button will have android:text="@+id/Button01". + // Here we detect if such an id is being remapped to a new id and if there's + // a text attribute with exactly the same id name, we update it too. + String oldText = null; + String oldId = null; + String newId = null; + + for (attr in oldElement.getAttributes()) { + String uri = attr.getUri(); + String name = attr.getName(); + String value = attr.getValue(); + + if (uri == ANDROID_URI) { + if (name == ATTR_ID) { + oldId = value; + } else if (name == ATTR_TEXT) { + oldText = value; + } + } + + def attrInfo = newNode.getAttributeInfo(uri, name); + if (attrInfo != null) { + def formats = attrInfo.getFormats(); + if (formats != null && IAttributeInfo.Format.REFERENCE in formats) { + if (idMap.containsKey(value)) { + value = idMap[value][0]; + } + } + } + + if (filter != null) { + value = filter(uri, name, value); + } + if (value != null && value != false && value != "") { + newNode.setAttribute(uri, name, value); + + if (uri == ANDROID_URI && name == ATTR_ID && oldId != null && value != oldId) { + newId = value; + } + } + } + + if (newId != null && oldText == oldId) { + newNode.setAttribute(ANDROID_URI, ATTR_TEXT, newId); + } + } + + /** + * Adds all the children elements of oldElement to newNode, recursively. + * Attributes are adjusted by calling addAttributes with idMap as necessary, with + * no closure filter. + */ + protected void addInnerElements(INode newNode, IDragElement oldElement, Map idMap) { + + for (element in oldElement.getInnerElements()) { + String fqcn = element.getFqcn(); + INode childNode = newNode.appendChild(fqcn); + + addAttributes(childNode, element, idMap, null /* closure */); + addInnerElements(childNode, element, idMap); + } + } + } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseView.groovy b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseView.groovy index dc37ffe..8a479d0 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseView.groovy +++ b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/BaseView.groovy @@ -20,11 +20,24 @@ public class BaseView implements IViewRule { private String mFqcn; + // Some common Android layout attribute names used by the view rules. + // All these belong to the attribute namespace ANDROID_URI. + public static String ATTR_ID = "id"; + public static String ATTR_TEXT = "text"; + public static String ATTR_LAYOUT_WIDTH = "layout_width"; + public static String ATTR_LAYOUT_HEIGHT = "layout_height"; + + // Some common Android layout attribute values used by the view rules. + public static String VALUE_FILL_PARENT = "fill_parent"; + public static String VALUE_MATCH_PARENT = "match_parent"; + public static String VALUE_MATCH_CONTENT = "match_content"; + + /** * Namespace for the Android resource XML, * i.e. "http://schemas.android.com/apk/res/android" */ - public static ANDROID_URI = "http://schemas.android.com/apk/res/android"; + public static String ANDROID_URI = "http://schemas.android.com/apk/res/android"; public boolean onInitialize(String fqcn) { // This base rule can handle any class. @@ -114,8 +127,7 @@ public class BaseView implements IViewRule { // ignore } - void onDropped(INode targetNode, IDragElement[] elements, DropFeedback feedback, - Point p, boolean isCopy, boolean sameCanvas) { + void onDropped(INode targetNode, IDragElement[] elements, DropFeedback feedback, Point p) { // ignore } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.AbsoluteLayout.groovy b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.AbsoluteLayout.groovy index c417ccb..ad3c862 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.AbsoluteLayout.groovy +++ b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.AbsoluteLayout.groovy @@ -86,18 +86,6 @@ public class AndroidWidgetAbsoluteLayoutRule extends BaseLayout { } } - void drawElement(IGraphics gc, IDragElement element, int offsetX, int offsetY) { - Rect b = element.getBounds(); - if (b.isValid()) { - b = b.copy().offsetBy(offsetX, offsetY); - gc.drawRect(b); - } - - element.getInnerElements().each { - drawElement(gc, it, offsetX, offsetY); - } - } - DropFeedback onDropMove(INode targetNode, IDragElement[] elements, DropFeedback feedback, @@ -116,9 +104,7 @@ public class AndroidWidgetAbsoluteLayoutRule extends BaseLayout { void onDropped(INode targetNode, IDragElement[] elements, DropFeedback feedback, - Point p, - boolean isCopy, - boolean sameCanvas) { + Point p) { Rect b = targetNode.getBounds(); if (!b.isValid()) { @@ -128,13 +114,9 @@ public class AndroidWidgetAbsoluteLayoutRule extends BaseLayout { int x = p.x - b.x; int y = p.y - b.y; - def id_map = [:]; - - // Need to remap ids if necessary - if (isCopy || !sameCanvas) { - collectIds(id_map, elements); - id_map = remapIds(targetNode, id_map); - } + // Collect IDs from dropped elements and remap them to new IDs + // if this is a copy or from a different canvas. + def id_map = getDropIdMap(targetNode, elements, feedback.isCopy || !feedback.sameCanvas); targetNode.editXml("Add elements to AbsoluteLayout") { @@ -149,8 +131,9 @@ public class AndroidWidgetAbsoluteLayoutRule extends BaseLayout { INode newChild = targetNode.appendChild(fqcn); // Copy all the attributes, modifying them as needed. - addAttributes(newChild, element.getAttributes(), id_map) { + addAttributes(newChild, element, id_map) { uri, name, value -> + // TODO exclude original parent attributes if (name == "layout_x" || name == "layout_y") { return false; // don't set these attributes } else { @@ -158,156 +141,25 @@ public class AndroidWidgetAbsoluteLayoutRule extends BaseLayout { } }; - targetNode.debugPrintf("POS: $offset , $x, $y, $be\n"); - if (first) { first = false; if (be.isValid()) { offset = new Point(x - be.x, y - be.y); - targetNode.debugPrintf("FIRST OFFSET: $offset\n"); } } else if (offset != null && be.isValid()) { x = offset.x + be.x; y = offset.y + be.y; - targetNode.debugPrintf("NEXT X,Y: $x , $y"); } else { x += 10; y += be.isValid() ? be.h : 10; - targetNode.debugPrintf("DEFAULT X,Y: $x , $y\n"); } newChild.setAttribute(ANDROID_URI, "layout_x", "${x}dip"); newChild.setAttribute(ANDROID_URI, "layout_y", "${y}dip"); - def children = element.getInnerElements(); - addInnerElements(newChild, element.getInnerElements(), id_map); - } - } - } - - void addAttributes(INode newNode, oldAttributes, id_map, Closure filter) { - for (attr in oldAttributes) { - String uri = attr.getUri(); - String name = attr.getName(); - String value = attr.getValue(); - - def attrInfo = newNode.getAttributeInfo(uri, name); - if (attrInfo != null) { - def formats = attrInfo.getFormats(); - if (formats != null && IAttributeInfo.Format.REFERENCE in formats) { - newNode.debugPrintf("REF attr: $name [$value]\n"); - if (id_map.containsKey(value)) { - value = id_map[value][0]; - } - } - } - - if (filter != null) { - value = filter(uri, name, value); - } - if (value != null && value != false && value != "") { - newNode.setAttribute(uri, name, value); + addInnerElements(newChild, element, id_map); } } } - void addInnerElements(INode node, IDragElement[] elements, id_map) { - - elements.each { element -> - String fqcn = element.getFqcn(); - INode newNode = node.appendChild(fqcn); - - addAttributes(newNode, element.getAttributes(), id_map, null /* closure */); - addInnerElements(newNode, element.getInnerElements(), id_map); - } - } - - /** - * Fills id_map with a map String id => tuple (String id, String fqcn) - * where fqcn is the FQCN of the element (in case we want to generate - * new IDs based on the element type.) - */ - void collectIds(id_map, IDragElement[] elements) { - elements.each { element -> - def attr = element.getAttribute(ANDROID_URI, "id"); - if (attr != null) { - String id = attr.getValue(); - if (id != null && id != "") { - id_map.put(id, [id, element.getFqcn()]); - } - } - - collectIds(id_map, element.getInnerElements()); - } - } - - Object remapIds(INode node, id_map) { - // Visit the document to get a list of existing ids - def existing_ids = [:]; - collectExistingIds(node.getRoot(), existing_ids); - - def new_map = [:]; - id_map.each() { key, value -> - def id = normalizeId(key); - - if (!existing_ids.containsKey(id)) { - // Not a conflict. Use as-is. - new_map.put(key, value); - if (key != id) { - new_map.put(id, value); - } - } else { - // There is a conflict. Get a new id. - def new_id = findNewId(value[1], existing_ids); - value[0] = new_id; - new_map.put(id, value); - new_map.put(id.replaceFirst("@\\+", "@"), value); - } - } - - return new_map; - } - - String findNewId(String fqcn, existing_ids) { - // Get the last component of the FQCN (e.g. "android.view.Button" => "Button") - String name = fqcn[fqcn.lastIndexOf(".")+1 .. fqcn.length()-1]; - - for (int i = 1; i < 1000000; i++) { - String id = String.format("@+id/%s%02d", name, i); - if (!existing_ids.containsKey(id)) { - existing_ids.put(id, id); - return id; - } - } - - // We'll never reach here. - return null; - } - - void collectExistingIds(INode root, existing_ids) { - if (root == null) { - return; - } - - def id = root.getStringAttr(ANDROID_URI, "id"); - if (id != null) { - id = normalizeId(id); - - if (!existing_ids.containsKey(id)) { - existing_ids.put(id, id); - } - } - - root.getChildren().each { - collectExistingIds(it, existing_ids); - } - } - - /** Transform @id/name into @+id/name to treat both forms the same way. */ - String normalizeId(String id) { - if (id.indexOf("@+") == -1) { - id = id.replaceFirst("@", "@+"); - } - return id; - } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.LinearLayout.groovy b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.LinearLayout.groovy index d723ac6..1f1b1c6 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.LinearLayout.groovy +++ b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.LinearLayout.groovy @@ -21,20 +21,28 @@ package com.android.adt.gscripts; */ public class AndroidWidgetLinearLayoutRule extends BaseLayout { + public static String ATTR_ORIENTATION = "orientation"; + public static String VALUE_VERTICAL = "vertical"; + // ==== Drag'n'drop support ==== - DropFeedback onDropEnter(INode targetNode, String fqcn) { + DropFeedback onDropEnter(INode targetNode, IDragElement[] elements) { + + if (elements.length == 0) { + return null; + } def bn = targetNode.getBounds(); if (!bn.isValid()) { return; } - boolean isVertical = targetNode.getStringAttr("orientation") == "vertical"; + boolean isVertical = + targetNode.getStringAttr(ANDROID_URI, ATTR_ORIENTATION) == VALUE_VERTICAL; // Prepare a list of insertion points: X coords for horizontal, Y for vertical. // Each list is a tuple: 0=pixel coordinate, 1=index of children or -1 for "at end". - def indexes = [ ] ; + def indexes = [ ]; int last = isVertical ? bn.y : bn.x; int pos = 0; @@ -57,56 +65,97 @@ public class AndroidWidgetLinearLayoutRule extends BaseLayout { return new DropFeedback( [ "isVertical": isVertical, // boolean: True if vertical linear layout "indexes": indexes, // list(tuple(0:int, 1:int)): insert points (pixels + index) - "curr_x": null, // int: Current marker X position - "curr_y": null, // int: Current marker Y position - "insert_pos": -1 // int: Current drop insert index (-1 for "at the end") + "currX": null, // int: Current marker X position + "currY": null, // int: Current marker Y position + "insertPos": -1 // int: Current drop insert index (-1 for "at the end") ], { gc, node, feedback -> // Paint closure for the LinearLayout. + // This is called by the canvas when a draw is needed. + + drawFeedback(gc, node, elements, feedback); + }); + } + + void drawFeedback(IGraphics gc, + INode node, + IDragElement[] elements, + DropFeedback feedback) { + Rect b = node.getBounds(); + if (!b.isValid()) { + return; + } + + // Highlight the receiver + gc.setForeground(gc.registerColor(0x00FFFF00)); + gc.setLineStyle(IGraphics.LineStyle.LINE_SOLID); + gc.setLineWidth(2); + gc.drawRect(b); + + gc.setLineStyle(IGraphics.LineStyle.LINE_DOT); + gc.setLineWidth(1); + + def indexes = feedback.userData.indexes; + boolean isVertical = feedback.userData.isVertical; - Rect b = node.getBounds(); - if (!b.isValid()) { - return; + indexes.each { + int i = it[0]; + if (isVertical) { + // draw horizontal lines + gc.drawLine(b.x, i, b.x + b.w, i); + } else { + // draw vertical lines + gc.drawLine(i, b.y, i, b.y + b.h); } + } + + def currX = feedback.userData.currX; + def currY = feedback.userData.currY; - gc.setForeground(gc.registerColor(0x00FFFF00)); + if (currX != null && currY != null) { + int x = currX; + int y = currY; + // Draw a mark at the drop point. gc.setLineStyle(IGraphics.LineStyle.LINE_SOLID); gc.setLineWidth(2); - gc.drawRect(b); - - gc.setLineStyle(IGraphics.LineStyle.LINE_DOT); - gc.setLineWidth(1); - - indexes.each { - int i = it[0]; - if (isVertical) { - // draw horizontal lines - gc.drawLine(b.x, i, b.x + b.w, i); - } else { - // draw vertical lines - gc.drawLine(i, b.y, i, b.y + b.h); - } - } - def curr_x = feedback.userData.curr_x; - def curr_y = feedback.userData.curr_y; + gc.drawLine(x - 10, y - 10, x + 10, y + 10); + gc.drawLine(x + 10, y - 10, x - 10, y + 10); + gc.drawOval(x - 10, y - 10, x + 10, y + 10); + + Rect be = elements[0].getBounds(); + + if (be.isValid()) { + // At least the first element has a bound. Draw rectangles + // for all dropped elements with valid bounds, offset at + // the drop point. - if (curr_x != null && curr_y != null) { - gc.setLineStyle(IGraphics.LineStyle.LINE_SOLID); - gc.setLineWidth(2); + int offsetX = x - be.x; + int offsetY = y - be.y; - int x = curr_x; - int y = curr_y; - gc.drawLine(x - 10, y - 10, x + 10, y + 10); - gc.drawLine(x + 10, y - 10, x - 10, y + 10); - gc.drawRect(x - 10, y - 10, x + 10, y + 10); + // If there's a parent, keep the X/Y coordinate the same relative to the parent. + Rect pb = elements[0].getParentBounds(); + if (pb.isValid()) { + if (isVertical) { + offsetX = b.x - pb.x; + } else { + offsetY = b.y - pb.y; + } + } + + for (element in elements) { + drawElement(gc, element, offsetX, offsetY); + } } - }) + } } - DropFeedback onDropMove(INode targetNode, String fqcn, DropFeedback feedback, Point p) { + DropFeedback onDropMove(INode targetNode, + IDragElement[] elements, + DropFeedback feedback, + Point p) { def data = feedback.userData; Rect b = targetNode.getBounds(); @@ -134,40 +183,71 @@ public class AndroidWidgetLinearLayoutRule extends BaseLayout { } if (bestIndex != Integer.MIN_VALUE) { - def old_x = data.curr_x; - def old_y = data.curr_y; + def old_x = data.currX; + def old_y = data.currY; if (isVertical) { - data.curr_x = b.x + b.w / 2; - data.curr_y = bestIndex; + data.currX = b.x + b.w / 2; + data.currY = bestIndex; } else { - data.curr_x = bestIndex; - data.curr_y = b.y + b.h / 2; + data.currX = bestIndex; + data.currY = b.y + b.h / 2; } - data.insert_pos = bestPos; + data.insertPos = bestPos; - feedback.requestPaint = (old_x != data.curr_x) || (old_y != data.curr_y); + feedback.requestPaint = (old_x != data.currX) || (old_y != data.currY); } return feedback; } - void onDropLeave(INode targetNode, String fqcn, DropFeedback feedback) { + void onDropLeave(INode targetNode, IDragElement[] elements, DropFeedback feedback) { // ignore } - void onDropped(INode targetNode, String fqcn, DropFeedback feedback, Point p) { - int insert_pos = feedback.userData.insert_pos; + void onDropped(INode targetNode, + IDragElement[] elements, + DropFeedback feedback, + Point p) { - targetNode.debugPrintf("Linear.drop: add ${fqcn} at position ${insert_pos}"); + int insertPos = feedback.userData.insertPos; - // Get the last component of the FQCN (e.g. "android.view.Button" => "Button") - String name = fqcn; - name = name[name.lastIndexOf(".")+1 .. name.length()-1]; + // Collect IDs from dropped elements and remap them to new IDs + // if this is a copy or from a different canvas. + def idMap = getDropIdMap(targetNode, elements, feedback.isCopy || !feedback.sameCanvas); - targetNode.editXml("Add ${name} to LinearLayout") { - INode e = targetNode.insertChildAt(fqcn, insert_pos); + targetNode.editXml("Add elements to LinearLayout") { + + // Now write the new elements. + for (element in elements) { + String fqcn = element.getFqcn(); + Rect be = element.getBounds(); + + INode newChild = targetNode.insertChildAt(fqcn, insertPos); + + // insertPos==-1 means to insert at the end. Otherwise + // increment the insertion position. + if (insertPos >= 0) { + insertPos++; + } + + // Copy all the attributes, modifying them as needed. + def attrFilter = getLayoutAttrFilter(); + addAttributes(newChild, element, idMap) { + uri, name, value -> + // TODO need a better way to exclude other layout attributes dynamically + if (uri == ANDROID_URI && name in attrFilter) { + return false; // don't set these attributes + } else { + return value; + } + }; + + addInnerElements(newChild, element, idMap); + } } + + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.ListView.groovy b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.ListView.groovy index 6325dfa..b7c00ca 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.ListView.groovy +++ b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.ListView.groovy @@ -30,8 +30,8 @@ public class AndroidWidgetListViewRule extends BaseView { * However ListView is special in that ideally we want fill_parent width by default. */ public Map<?, ?> getDefaultAttributes() { - // TODO: find a way to plug in the new value match_parent. - return [ "layout_width" : "fill_parent" ]; + // TODO: find a way to plug in the new value VALUE_MATCH_PARENT. + return [ ATTR_LAYOUT_WIDTH : VALUE_FILL_PARENT ]; } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.RelativeLayout.groovy b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.RelativeLayout.groovy index 643047f..02606d9 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.RelativeLayout.groovy +++ b/eclipse/plugins/com.android.ide.eclipse.adt/gscripts/android.widget.RelativeLayout.groovy @@ -49,7 +49,7 @@ public class AndroidWidgetRelativeLayoutRule extends BaseLayout { def infos = []; def addAttr = { - def a = childNode.getStringAttr("layout_${it}"); + def a = childNode.getStringAttr(ANDROID_URI, "layout_${it}"); if (a) { infos += "${it}: ${a}"; } @@ -88,28 +88,40 @@ public class AndroidWidgetRelativeLayoutRule extends BaseLayout { // ==== Drag'n'drop support ==== - DropFeedback onDropEnter(INode targetNode, String fqcn) { + DropFeedback onDropEnter(INode targetNode, IDragElement[] elements) { + + if (elements.length == 0) { + return null; + } def bn = targetNode.getBounds(); if (!bn.isValid()) { return; } + // Collect the ids of the elements being dragged + def movedIds = collectIds([:], elements).keySet().asList(); + // Prepare the drop feedback return new DropFeedback( [ "child": null, // INode: Current child under cursor "index": 0, // int: Index of child in the parent children list "zones": null, // Valid "anchor" zones for the current child // of type list(map(rect:Rect, attr:[String])) - "curr": null, // map: Current zone + "curr": null, // map: Current zone + "movedIds": movedIds, + "cachedLinkIds": [:] ], { gc, node, feedback -> // Paint closure for the RelativeLayout just defers to the method below - drawRelativeDropFeedback(gc, node, feedback); + drawRelativeDropFeedback(gc, node, elements, feedback); }); } - DropFeedback onDropMove(INode layoutNode, String fqcn, DropFeedback feedback, Point p) { + DropFeedback onDropMove(INode targetNode, + IDragElement[] elements, + DropFeedback feedback, + Point p) { def data = feedback.userData; Rect area = feedback.captureArea; @@ -123,9 +135,35 @@ public class AndroidWidgetRelativeLayoutRule extends BaseLayout { // Find the current direct children under the cursor def childNode = null; def childIndex = -1; - for(child in layoutNode.getChildren()) { + nextChild: for(child in targetNode.getChildren()) { childIndex++; - if (child.getBounds().contains(p)) { + def bc = child.getBounds(); + if (bc.contains(p)) { + + // TODO visually indicate this target node has been rejected, + // e.g. by drawing a semi-transp rect on it or drawing a red cross at + // the cursor point. + + // If we're doing a move operation within the same canvas, we can't + // attach the moved object to one belonging to the selection since + // it will disappear after the move. + if (feedback.sameCanvas && !feedback.isCopy) { + for (element in elements) { + if (bc == element.getBounds()) { + continue nextChild; + } + } + } + + // One more limitation: if we're moving one or more elements, we can't + // drop them on a child which relative position is expressed directly or + // indirectly based on the element being moved. + if (!feedback.isCopy) { + if (searchRelativeIds(child, data.movedIds, data.cachedLinkIds)) { + continue nextChild; + } + } + childNode = child; break; } @@ -151,7 +189,7 @@ public class AndroidWidgetRelativeLayoutRule extends BaseLayout { data.index = -1; data.curr = null; - def zone = computeBorderDropZone(layoutNode.getBounds(), p); + def zone = computeBorderDropZone(targetNode.getBounds(), p); if (zone == null) { data.zones = null; @@ -183,6 +221,71 @@ public class AndroidWidgetRelativeLayoutRule extends BaseLayout { return feedback; } + /** + * Returns true if the child has any attribute of Format.REFERENCE which + * value matches one of the ids in movedIds. + */ + def searchRelativeIds(INode node, List movedIds, Map cachedLinkIds) { + def ids = getLinkedIds(node, cachedLinkIds); + + for (id in ids) { + if (id in movedIds) { + return true; + } + } + + return false; + } + + def getLinkedIds(INode node, Map cachedLinkIds) { + def ids = cachedLinkIds[node]; + + if (ids != null) { + return ids; + } + + // we don't have cached data on this child, so create a list of + // all the linked id it is referencing. + ids = []; + cachedLinkIds[node] = ids; + for (attr in node.getAttributes()) { + def attrInfo = node.getAttributeInfo(attr.getUri(), attr.getName()); + if (attrInfo == null) { + continue; + } + def formats = attrInfo.getFormats(); + if (formats == null || !(IAttributeInfo.Format.REFERENCE in formats)) { + continue; + } + def id = attr.getValue(); + id = normalizeId(id); + if (id in ids) { + continue; + } + ids.add(id); + + // Find the sibling with that id + def p = node.getParent(); + if (p == null) { + continue; + } + for (child in p.getChildren()) { + if (child == node) { + continue; + } + def childId = child.getStringAttr(ANDROID_URI, ATTR_ID); + childId = normalizeId(childId); + if (id == childId) { + def linkedIds = getLinkedIds(child, cachedLinkIds); + ids.addAll(linkedIds); + break; + } + } + } + + return ids; + } + def computeBorderDropZone(Rect bounds, Point p) { int x = p.x; @@ -320,8 +423,11 @@ public class AndroidWidgetRelativeLayoutRule extends BaseLayout { return [ bounds, zones ]; } - void drawRelativeDropFeedback(IGraphics gc, INode layoutNode, DropFeedback feedback) { - Rect b = layoutNode.getBounds(); + void drawRelativeDropFeedback(IGraphics gc, + INode targetNode, + IDragElement[] elements, + DropFeedback feedback) { + Rect b = targetNode.getBounds(); if (!b.isValid()) { return; } @@ -356,7 +462,7 @@ public class AndroidWidgetRelativeLayoutRule extends BaseLayout { int h = gc.getFontHeight(); String id = null; if (data.child) { - id = data.child.getStringAttr("id"); + id = data.child.getStringAttr(ANDROID_URI, ATTR_ID); } data.curr.attr.each { String s = it; @@ -376,41 +482,87 @@ public class AndroidWidgetRelativeLayoutRule extends BaseLayout { y = mark.y; gc.drawLine(x - 10, y - 10, x + 10, y + 10); gc.drawLine(x + 10, y - 10, x - 10, y + 10); - gc.drawRect(x - 10, y - 10, x + 10, y + 10); - } + gc.drawOval(x - 10, y - 10, x + 10, y + 10); + + Rect be = elements[0].getBounds(); + + if (be.isValid()) { + // At least the first element has a bound. Draw rectangles + // for all dropped elements with valid bounds, offset at + // the drop point. + + int offsetX = x - be.x; + int offsetY = y - be.y; + + gc.setForeground(gc.registerColor(0x00FFFF00)); + for (element in elements) { + drawElement(gc, element, offsetX, offsetY); + } + } + } } } - void onDropLeave(INode targetNode, String fqcn, DropFeedback feedback) { + void onDropLeave(INode targetNode, IDragElement[] elements, DropFeedback feedback) { // Free the last captured rect, if any feedback.captureArea = null; } - void onDropped(INode targetNode, String fqcn, DropFeedback feedback, Point p) { + void onDropped(INode targetNode, + IDragElement[] elements, + DropFeedback feedback, + Point p) { def data = feedback.userData; if (!data.curr) { return; } def index = data.index; + def insertPos = data.insertPos; - targetNode.debugPrintf("Relative.drop: add ${fqcn} after index ${index}"); + // Collect IDs from dropped elements and remap them to new IDs + // if this is a copy or from a different canvas. + def idMap = getDropIdMap(targetNode, elements, feedback.isCopy || !feedback.sameCanvas); - // Get the last component of the FQCN (e.g. "android.view.Button" => "Button") - String name = fqcn; - name = name[name.lastIndexOf(".")+1 .. name.length()-1]; + targetNode.editXml("Add elements to RelativeLayout") { - targetNode.editXml("Add ${name} to RelativeLayout") { - INode e = targetNode.insertChildAt(fqcn, index + 1); + // Now write the new elements. + for (element in elements) { + String fqcn = element.getFqcn(); + Rect be = element.getBounds(); - String id = null; - if (data.child) { - id = data.child.getStringAttr("id"); - } + // index==-1 means to insert at the end. + // Otherwise increment the insertion position. + if (index >= 0) { + index++; + } - data.curr.attr.each { - e.setAttribute("layout_${it}", id ? id : "true"); + INode newChild = targetNode.insertChildAt(fqcn, index); + + // Copy all the attributes, modifying them as needed. + def attrFilter = getLayoutAttrFilter(); + addAttributes(newChild, element, idMap) { + uri, name, value -> + // TODO need a better way to exclude other layout attributes dynamically + if (uri == ANDROID_URI && name in attrFilter) { + return false; // don't set these attributes + } else { + return value; + } + }; + +// TODO... seems totally wrong. REVISIT or EXPLAIN + String id = null; + if (data.child) { + id = data.child.getStringAttr(ANDROID_URI, ATTR_ID); + } + + data.curr.attr.each { + newChild.setAttribute(ANDROID_URI, "layout_${it}", id ? id : "true"); + } + + addInnerElements(newChild, element, idMap); } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/DropFeedback.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/DropFeedback.java index 315caf4..82a07a9 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/DropFeedback.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/DropFeedback.java @@ -52,6 +52,19 @@ public class DropFeedback { public Rect captureArea; /** + * Set to true by the drag'n'drop engine when the current drag operation is a copy. + * When false the operation is a move and <em>after</em> a successful drop the source + * elements will be deleted. + */ + public boolean isCopy; + + /** + * Set to true when the drag'n'drop starts and ends in the same canvas of the + * same Eclipse instance. + */ + public boolean sameCanvas; + + /** * Initializes the drop feedback with the given user data and paint closure. * A paint is requested if the paint closure is non-null. */ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IDragElement.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IDragElement.java index b9be937..2a91401 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IDragElement.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IDragElement.java @@ -35,6 +35,8 @@ public interface IDragElement { * Returns the bounds of the element's node, if it originated from an existing * canvas. The rectangle is invalid and non-null when the element originated * from the object palette. + * + * The bounds are absolute for the canvas. */ public abstract Rect getBounds(); @@ -46,6 +48,14 @@ public interface IDragElement { public abstract String getParentFqcn(); /** + * Returns the bounds of the element's parent, absolute for the canvas, or invalid if there + * is no suitable parent. This is generally invalid when {@link #getParentFqcn()} is null. + * + * The returned rectangle can be invalid. It is never null. + */ + public abstract Rect getParentBounds(); + + /** * Returns a list of attributes. The list can be empty but is never null. */ public abstract IDragAttribute[] getAttributes(); @@ -62,10 +72,18 @@ public interface IDragElement { /** * An XML attribute in the {@link IDragElement}. + * <p/> + * The attribute is always represented by a namespace URI, a name and a value. + * The name cannot be empty. + * The namespace URI can be empty for an attribute without a namespace but is never null. + * The value can be empty but cannot be null. */ public interface IDragAttribute { - /** Returns the namespace URI of the attribute. Cannot be null nor empty. */ + /** + * Returns the namespace URI of the attribute. + * Can be empty for an attribute without a namespace but is never null. + */ public abstract String getUri(); /** Returns the XML local name of the attribute. Cannot be null nor empty. */ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/INode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/INode.java index acd9327..b850d60 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/INode.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/INode.java @@ -17,6 +17,8 @@ package com.android.ide.eclipse.adt.editors.layout.gscripts; +import com.android.ide.eclipse.adt.editors.layout.gscripts.IDragElement.IDragAttribute; + import groovy.lang.Closure; @@ -168,10 +170,35 @@ public interface INode { */ public IAttributeInfo getAttributeInfo(String uri, String attrName); + + /** + * Returns the list of all attributes defined in the XML for this node. + * <p/> + * This looks up an attribute in the <em>current</em> XML source, not the in-memory model. + * That means that if called in the context of {@link #editXml(String, Closure)}, the value + * returned here is not affected by {@link #setAttribute(String, String, String)} until + * the editXml closure is completed and the actual XML is updated. + * + * @return A non-null possible-empty list of {@link IAttribute}. + */ + public IAttribute[] getAttributes(); + // ----------- /** TODO: this is a hack. Shouldn't be here but instead part of some kind of helper * given to IViewRule implementations. */ void debugPrintf(String msg, Object...params); + + // ----------- + + /** + * An XML attribute in an {@link INode}. + * <p/> + * The attribute is always represented by a namespace URI, a name and a value. + * The name cannot be empty. + * The namespace URI can be empty for an attribute without a namespace but is never null. + * The value can be empty but cannot be null. + */ + public static interface IAttribute extends IDragAttribute { } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IViewRule.java index 9a3bce3..6200d8a 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IViewRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/IViewRule.java @@ -170,8 +170,6 @@ public interface IViewRule { void onDropped(INode targetNode, IDragElement[] elements, DropFeedback feedback, - Point where, - boolean isCopy, - boolean sameCanvas); + Point where); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/Rect.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/Rect.java index f98c4eb..a238d14 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/Rect.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/editors/layout/gscripts/Rect.java @@ -125,6 +125,15 @@ public class Rect { public boolean equals(Object obj) { if (obj instanceof Rect) { Rect rhs = (Rect) obj; + // validity must be equal on both sides. + if (isValid() != rhs.isValid()) { + return false; + } + // an invalid rect is equal to any other invalid rect regardless of coordinates + if (!isValid() && !rhs.isValid()) { + return true; + } + return this.x == rhs.x && this.y == rhs.y && this.w == rhs.w && this.h == rhs.h; } else if (obj instanceof Rectangle) { 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/CanvasDropListener.java index 29a148d..ee0b802 100755 --- 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/CanvasDropListener.java @@ -209,6 +209,8 @@ import java.util.Arrays; public void dropAccept(DropTargetEvent event) { AdtPlugin.printErrorToConsole("DEBUG", "drop accept"); + checkDataType(event); + // If we have a valid target node and it matches the one we saved in // dragLeave then we restore the DropFeedback that we saved in dragLeave. if (mLeaveTargetNode != null) { @@ -216,17 +218,14 @@ import java.util.Arrays; mFeedback = mLeaveFeedback; mCurrentView = mLeaveView; } - mLeaveTargetNode = null; - mLeaveFeedback = null; - mLeaveView = null; - - checkDataType(event); - if (event.detail != DND.DROP_NONE) { - processDropEvent(event); - } else { + if (mLeaveTargetNode == null || event.detail == DND.DROP_NONE) { clearDropInfo(); } + + mLeaveTargetNode = null; + mLeaveFeedback = null; + mLeaveView = null; } /* @@ -263,21 +262,32 @@ import java.util.Arrays; Point where = mCanvas.displayToCanvasPoint(event.x, event.y); - boolean isCopy = event.detail == DND.DROP_COPY; - boolean sameCanvas = mCanvas == mGlobalDragInfo.getSourceCanvas(); - + updateDropFeedback(mFeedback, event); mCanvas.getRulesEngine().callOnDropped(mTargetNode, elements, mFeedback, - where, - isCopy, - sameCanvas); + where); clearDropInfo(); mCanvas.redraw(); } /** + * Updates the {@link DropFeedback#isCopy} and {@link DropFeedback#sameCanvas} fields + * of the given {@link DropFeedback}. This is generally called right before invoking + * one of the callOnXyz methods of GRE to refresh the fields. + * + * @param df The current {@link DropFeedback}. + * @param event An optional event to determine if the current operaiton is copy or move. + */ + private void updateDropFeedback(DropFeedback df, DropTargetEvent event) { + if (event != null) { + df.isCopy = event.detail == DND.DROP_COPY; + } + df.sameCanvas = mCanvas == mGlobalDragInfo.getSourceCanvas(); + } + + /** * Invoked by the canvas to refresh the display. * @param gCWrapper The GC wrapper, never null. */ @@ -371,37 +381,46 @@ import java.util.Arrays; } else { // vi is a new current view. - // Query GRE for onDropEnter on the view till we find one that returns a non-null - // object. + // Query GRE for onDropEnter on the ViewInfo hierarchy, starting from the child + // towards its parent, till we find one that returns a non-null drop feedback. DropFeedback df = null; NodeProxy targetNode = null; - boolean invalidTarget = false; - - if (mCanvas == mGlobalDragInfo.getSourceCanvas()) { - // If we're drag'n'drop in the same canvas, none of the objects being - // dragged should be considered as valid targets. - CanvasSelection[] selection = mGlobalDragInfo.getCurrentSelection(); - if (selection != null) { - for (CanvasSelection cs : selection) { - if (cs.getViewInfo() == vi) { - invalidTarget = true; - break; + + for (CanvasViewInfo targetVi = vi; + targetVi != null && df == null; + targetVi = targetVi.getParent()) { + targetNode = mCanvas.getNodeFactory().create(targetVi); + df = mCanvas.getRulesEngine().callOnDropEnter(targetNode, + mCurrentDragElements); + + if (df != null && + event.detail == DND.DROP_MOVE && + mCanvas == mGlobalDragInfo.getSourceCanvas()) { + // You can't move an object into itself in the same canvas. + // E.g. case of moving a layout and the node under the mouse is the + // layout itself: a copy would be ok but not a move operation of the + // layout into himself. + + CanvasSelection[] selection = mGlobalDragInfo.getCurrentSelection(); + if (selection != null) { + for (CanvasSelection cs : selection) { + if (cs.getViewInfo() == targetVi) { + // The node that responded is one of the selection roots. + // Simply invalidate the drop feedback and move on the + // parent in the ViewInfo chain. + + updateDropFeedback(df, event); + mCanvas.getRulesEngine().callOnDropLeave( + targetNode, mCurrentDragElements, df); + df = null; + targetNode = null; + } } } } } - if (!invalidTarget) { - for (CanvasViewInfo targetVi = vi; - targetVi != null && df == null; - targetVi = targetVi.getParent()) { - targetNode = mCanvas.getNodeFactory().create(targetVi); - df = mCanvas.getRulesEngine().callOnDropEnter(targetNode, - mCurrentDragElements); - } - } - if (df != null && targetNode != mTargetNode) { // We found a new target node for the drag'n'drop. // Release the previous one, if any. @@ -435,11 +454,14 @@ import java.util.Arrays; // this is a move inside the same view com.android.ide.eclipse.adt.editors.layout.gscripts.Point p2 = new com.android.ide.eclipse.adt.editors.layout.gscripts.Point(x, y); + updateDropFeedback(mFeedback, event); DropFeedback df = mCanvas.getRulesEngine().callOnDropMove( mTargetNode, mCurrentDragElements, mFeedback, p2); if (df == null) { // The target is no longer interested in the drop move. callDropLeave(); + } else if (df != mFeedback) { + mFeedback = df; } } @@ -454,6 +476,7 @@ import java.util.Arrays; */ private void callDropLeave() { if (mTargetNode != null && mFeedback != null) { + updateDropFeedback(mFeedback, null); mCanvas.getRulesEngine().callOnDropLeave(mTargetNode, mCurrentDragElements, mFeedback); } 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 d043664..fb5ee04 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 @@ -1218,13 +1218,17 @@ import java.util.ListIterator; 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); + SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds); for (UiAttributeNode attr : uiNode.getUiAttributes()) { String value = attr.getCurrentValue(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteComposite.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteComposite.java index 4073d8a..0e16105 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteComposite.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteComposite.java @@ -431,7 +431,10 @@ public class PaletteComposite extends Composite { public DescDragSourceListener(ElementDescriptor desc) { SimpleElement se = new SimpleElement( - SimpleXmlTransfer.getFqcn(desc), null /* parentFqcn */, null /* bounds */); + SimpleXmlTransfer.getFqcn(desc), + null /* parentFqcn */, + null /* bounds */, + null /* parentBounds */); mElements = new SimpleElement[] { se }; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java index d9edf30..8dc81a6 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleAttribute.java @@ -17,6 +17,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import com.android.ide.eclipse.adt.editors.layout.gscripts.IDragElement.IDragAttribute; +import com.android.ide.eclipse.adt.editors.layout.gscripts.INode.IAttribute; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -24,31 +25,39 @@ import java.util.regex.Pattern; /** * Represents one XML attribute in a {@link SimpleElement}. * <p/> - * The attribute is always represented by a namespace URI, a name and a value, none of which - * can be null. The name and URI cannot be empty. + * The attribute is always represented by a namespace URI, a name and a value. + * The name cannot be empty. + * The namespace URI can be empty for an attribute without a namespace but is never null. + * The value can be empty but cannot be null. * <p/> * For a more detailed explanation of the purpose of this class, * please see {@link SimpleXmlTransfer}. */ -public class SimpleAttribute implements IDragAttribute { +public class SimpleAttribute implements IDragAttribute, IAttribute { private final String mName; private final String mValue; private final String mUri; /** * Creates a new {@link SimpleAttribute}. + * <p/> + * Any null value will be converted to an empty non-null string. + * However it is a semantic error to use an empty name -- no assertion is done though. * - * @param uri The URI of the attribute. Cannot be null nor empty. - * @param name The XML local name of the attribute. Cannot be null nor empty. - * @param value The value of the attribute. Can be empty but not null. + * @param uri The URI of the attribute. + * @param name The XML local name of the attribute. + * @param value The value of the attribute. */ public SimpleAttribute(String uri, String name, String value) { - mUri = uri; - mName = name; - mValue = value; + mUri = uri == null ? "" : uri; + mName = name == null ? "" : name; + mValue = value == null ? "" : value; } - /** Returns the namespace URI of the attribute. Cannot be null nor empty. */ + /** + * Returns the namespace URI of the attribute. + * Can be empty for an attribute without a namespace but is never null. + */ public String getUri() { return mUri; } @@ -67,11 +76,14 @@ public class SimpleAttribute implements IDragAttribute { @Override public String toString() { - return String.format("@%s:%s=%s\n", mName, mUri, mValue); //$NON-NLS-1$ + return String.format("@%s:%s=%s\n", //$NON-NLS-1$ + mName, + mUri, + mValue); } private static final Pattern REGEXP = - Pattern.compile("[^@]*@([^:]+):([^=]+)=([^\n]*)\n*"); //$NON-NLS-1$ + Pattern.compile("[^@]*@([^:]+):([^=]*)=([^\n]*)\n*"); //$NON-NLS-1$ static SimpleAttribute parseString(String value) { Matcher m = REGEXP.matcher(value); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java index 8103973..fd06726 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElement.java @@ -33,11 +33,12 @@ import java.util.ArrayList; public class SimpleElement implements IDragElement { /** Version number of the internal serialized string format. */ - private static final String FORMAT_VERSION = "2"; + private static final String FORMAT_VERSION = "3"; private final String mFqcn; private final String mParentFqcn; private final Rect mBounds; + private final Rect mParentBounds; private final ArrayList<IDragAttribute> mAttributes = new ArrayList<IDragAttribute>(); private final ArrayList<IDragElement> mElements = new ArrayList<IDragElement>(); @@ -53,11 +54,13 @@ public class SimpleElement implements IDragElement { * Can be null but not empty. * @param bounds The canvas bounds of the originating canvas node of the element. * If null, a non-null invalid rectangle will be assigned. + * @param parentBounds The canvas bounds of the parent of this element. Can be null. */ - public SimpleElement(String fqcn, String parentFqcn, Rect bounds) { + public SimpleElement(String fqcn, String parentFqcn, Rect bounds, Rect parentBounds) { mFqcn = fqcn; mParentFqcn = parentFqcn; mBounds = bounds == null ? new Rect() : bounds.copy(); + mParentBounds = parentBounds == null ? new Rect() : parentBounds.copy(); } /** @@ -86,6 +89,14 @@ public class SimpleElement implements IDragElement { return mParentFqcn; } + /** + * Returns the bounds of the element's parent, absolute for the canvas, or null if there + * is no suitable parent. This is null when {@link #getParentFqcn()} is null. + */ + public Rect getParentBounds() { + return mParentBounds; + } + public IDragAttribute[] getAttributes() { if (mCachedAttributes == null) { mCachedAttributes = mAttributes.toArray(new IDragAttribute[mAttributes.size()]); @@ -132,7 +143,11 @@ public class SimpleElement implements IDragElement { } if (mBounds != null && mBounds.isValid()) { sb.append(String.format(",R=%d %d %d %d", mBounds.x, mBounds.y, mBounds.w, mBounds.h)); - } + } + if (mParentBounds != null && mParentBounds.isValid()) { + sb.append(String.format(",Q=%d %d %d %d", + mParentBounds.x, mParentBounds.y, mParentBounds.w, mParentBounds.h)); + } sb.append('\n'); for (IDragAttribute a : mAttributes) { sb.append(a.toString()); @@ -174,6 +189,7 @@ public class SimpleElement implements IDragElement { String fqcn = null; String parent = null; Rect bounds = null; + Rect pbounds = null; for (String s2 : s.substring(1).split(",")) { //$NON-NLS-1$ int pos = s2.indexOf('='); @@ -198,18 +214,24 @@ public class SimpleElement implements IDragElement { } else if (key.equals("P")) { //$NON-NLS-1$ parent = value; - } else if (key.equals("R")) { //$NON-NLS-1$ + } else if (key.equals("R") || key.equals("Q")) { //$NON-NLS-1$ //$NON-NLS-2$ // Parse the canvas bounds String[] sb = value.split(" +"); //$NON-NLS-1$ if (sb != null && sb.length == 4) { + Rect r = null; try { - bounds = new Rect(); - bounds.x = Integer.parseInt(sb[0]); - bounds.y = Integer.parseInt(sb[1]); - bounds.w = Integer.parseInt(sb[2]); - bounds.h = Integer.parseInt(sb[3]); + r = new Rect(); + r.x = Integer.parseInt(sb[0]); + r.y = Integer.parseInt(sb[1]); + r.w = Integer.parseInt(sb[2]); + r.h = Integer.parseInt(sb[3]); + + if (key.equals("R")) { + bounds = r; + } else { + pbounds = r; + } } catch (NumberFormatException ignore) { - bounds = null; } } } @@ -217,7 +239,7 @@ public class SimpleElement implements IDragElement { // We need at least a valid name to recreate an element if (version != null && fqcn != null && fqcn.length() > 0) { - e = new SimpleElement(fqcn, parent, bounds); + e = new SimpleElement(fqcn, parent, bounds, pbounds); } } else { // This is an inner element... need to parse the { line again. @@ -259,6 +281,10 @@ public class SimpleElement implements IDragElement { (mParentFqcn != null && !mParentFqcn.equals(se.mParentFqcn))) { return false; } + if ((mParentBounds == null && se.mParentBounds != null) || + (mParentBounds != null && !mParentBounds.equals(se.mParentBounds))) { + return false; + } return mFqcn.equals(se.mFqcn) && mAttributes.size() == se.mAttributes.size() && @@ -278,9 +304,12 @@ public class SimpleElement implements IDragElement { if (mParentFqcn != null) { c = 31*c + mParentFqcn.hashCode(); } - if (mBounds != null) { + if (mBounds != null && mBounds.isValid()) { c = 31*c + mBounds.hashCode(); } + if (mParentBounds != null && mParentBounds.isValid()) { + c = 31*c + mParentBounds.hashCode(); + } if (c > 0x0FFFFFFFFL) { // wrap any overflow 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 5f1b889..f95e44d 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 @@ -28,6 +28,7 @@ import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescripto import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleAttribute; 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; @@ -317,6 +318,33 @@ public class NodeProxy implements INode { return null; } + public IAttribute[] getAttributes() { + UiElementNode uiNode = mNode; + + if (uiNode.getXmlNode() != null) { + Node xmlNode = uiNode.getXmlNode(); + if (xmlNode != null) { + NamedNodeMap nodeAttributes = xmlNode.getAttributes(); + if (nodeAttributes != null) { + + int n = nodeAttributes.getLength(); + IAttribute[] result = new IAttribute[n]; + for (int i = 0; i < n; i++) { + Node attr = nodeAttributes.item(i); + String uri = attr.getNamespaceURI(); + String name = attr.getLocalName(); + String value = attr.getNodeValue(); + + result[i] = new SimpleAttribute(uri, name, value); + } + return result; + } + } + } + return null; + + } + // --- internal helpers --- diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java index 01e2515..6dd2a79 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java @@ -294,15 +294,13 @@ public class RulesEngine { public void callOnDropped(NodeProxy targetNode, IDragElement[] elements, DropFeedback feedback, - Point where, - boolean isCopy, - boolean sameCanvas) { + Point where) { // try to find a rule for this element's FQCN IViewRule rule = loadRule(targetNode.getNode()); if (rule != null) { try { - rule.onDropped(targetNode, elements, feedback, where, isCopy, sameCanvas); + rule.onDropped(targetNode, elements, feedback, where); } catch (Exception e) { logError("%s.onDropped() failed: %s", diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElementTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElementTest.java index d97a230..7c098bd 100755 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElementTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleElementTest.java @@ -58,7 +58,8 @@ public class SimpleElementTest extends TestCase { e = new SimpleElement("android.view.LinearLayout", // fqcn "android.view.FrameLayout", // parentFqcn - new Rect(10, 5, 60, 40)); + new Rect(10, 5, 60, 40), // bounds + new Rect(0, 0, 320, 480)); // parentBounds } public final void testGetFqcn() { @@ -73,15 +74,19 @@ public class SimpleElementTest extends TestCase { assertEquals(new Rect(10, 5, 60, 40), e.getBounds()); } + public final void testGetParentBounds() { + assertEquals(new Rect(0, 0, 320, 480), e.getParentBounds()); + } + public final void testToString() { - assertEquals("{V=2,N=android.view.LinearLayout,P=android.view.FrameLayout,R=10 5 60 40\n" + + assertEquals("{V=3,N=android.view.LinearLayout,P=android.view.FrameLayout,R=10 5 60 40,Q=0 0 320 480\n" + "}\n", e.toString()); e.addAttribute(new SimpleAttribute("uri", "name", "value")); e.addAttribute(new SimpleAttribute("my-uri", "second-name", "my = value ")); - assertEquals("{V=2,N=android.view.LinearLayout,P=android.view.FrameLayout,R=10 5 60 40\n" + + assertEquals("{V=3,N=android.view.LinearLayout,P=android.view.FrameLayout,R=10 5 60 40,Q=0 0 320 480\n" + "@name:uri=value\n" + "@second-name:my-uri=my = value \n" + "}\n", @@ -89,23 +94,25 @@ public class SimpleElementTest extends TestCase { SimpleElement e2 = new SimpleElement("android.view.Button", "android.view.LinearLayout", - new Rect(10, 20, 30, 40)); + new Rect(10, 20, 30, 40), + new Rect(0, 0, 320, 480)); e2.addAttribute(new SimpleAttribute("uri1", "name1", "value1")); SimpleElement e3 = new SimpleElement("android.view.CheckBox", "android.view.LinearLayout", - new Rect(-1, -2, -3, -4)); // invalid rect is ignored + new Rect(-1, -2, -3, -4), // invalid rect is ignored + new Rect(-1, -2, -3, -4)); // invalid rectis ignored e3.addAttribute(new SimpleAttribute("uri2", "name2", "value2")); e3.addAttribute(new SimpleAttribute("uri3", "name3", "value3")); e.addInnerElement(e2); e.addInnerElement(e3); - assertEquals("{V=2,N=android.view.LinearLayout,P=android.view.FrameLayout,R=10 5 60 40\n" + + assertEquals("{V=3,N=android.view.LinearLayout,P=android.view.FrameLayout,R=10 5 60 40,Q=0 0 320 480\n" + "@name:uri=value\n" + "@second-name:my-uri=my = value \n" + - "{V=2,N=android.view.Button,P=android.view.LinearLayout,R=10 20 30 40\n" + + "{V=3,N=android.view.Button,P=android.view.LinearLayout,R=10 20 30 40,Q=0 0 320 480\n" + "@name1:uri1=value1\n" + "}\n" + - "{V=2,N=android.view.CheckBox,P=android.view.LinearLayout\n" + + "{V=3,N=android.view.CheckBox,P=android.view.LinearLayout\n" + "@name2:uri2=value2\n" + "@name3:uri3=value3\n" + "}\n" + @@ -115,32 +122,34 @@ public class SimpleElementTest extends TestCase { public final void testParseString() { assertArrayEquals( - new SimpleElement[] { new SimpleElement("android.view.LinearLayout", null, null) }, + new SimpleElement[] { new SimpleElement("android.view.LinearLayout", + null, null, null) }, SimpleElement.parseString( - "{V=2,N=android.view.LinearLayout\n" + + "{V=3,N=android.view.LinearLayout\n" + "}\n")); assertArrayEquals( new SimpleElement[] { new SimpleElement("android.view.LinearLayout", "android.view.FrameLayout", - null) }, + null, null) }, SimpleElement.parseString( - "{V=2,N=android.view.LinearLayout,P=android.view.FrameLayout\n" + + "{V=3,N=android.view.LinearLayout,P=android.view.FrameLayout\n" + "}\n")); assertArrayEquals( new SimpleElement[] { new SimpleElement("android.view.LinearLayout", null, - new Rect(10, 5, 60, 40)) }, + new Rect(10, 5, 60, 40), + new Rect(0, 0, 320, 480)) }, SimpleElement.parseString( - "{V=2,N=android.view.LinearLayout,R=10 5 60 40\n" + + "{V=3,N=android.view.LinearLayout,R=10 5 60 40,Q=0 0 320 480\n" + "}\n")); assertArrayEquals( new SimpleElement[] { e }, SimpleElement.parseString( - "{V=2,N=android.view.LinearLayout,P=android.view.FrameLayout,R=10 5 60 40\n" + + "{V=3,N=android.view.LinearLayout,P=android.view.FrameLayout,R=10 5 60 40,Q=0 0 320 480\n" + "}\n")); @@ -150,7 +159,7 @@ public class SimpleElementTest extends TestCase { assertArrayEquals( new SimpleElement[] { e }, SimpleElement.parseString( - "{V=2,N=android.view.LinearLayout,P=android.view.FrameLayout,R=10 5 60 40\n" + + "{V=3,N=android.view.LinearLayout,P=android.view.FrameLayout,R=10 5 60 40,Q=0 0 320 480\n" + "@name:uri=value\n" + "@second-name:my-uri=my = value \n" + "}\n")); @@ -158,10 +167,12 @@ public class SimpleElementTest extends TestCase { SimpleElement e2 = new SimpleElement("android.view.Button", "android.view.LinearLayout", - new Rect(10, 20, 30, 40)); + new Rect(10, 20, 30, 40), + new Rect(0, 0, 320, 480)); e2.addAttribute(new SimpleAttribute("uri1", "name1", "value1")); SimpleElement e3 = new SimpleElement("android.view.CheckBox", "android.view.LinearLayout", + new Rect(-1, -2, -3, -4), new Rect(-1, -2, -3, -4)); e3.addAttribute(new SimpleAttribute("uri2", "name2", "value2")); e3.addAttribute(new SimpleAttribute("uri3", "name3", "value3")); @@ -171,13 +182,13 @@ public class SimpleElementTest extends TestCase { assertArrayEquals( new SimpleElement[] { e }, SimpleElement.parseString( - "{V=2,N=android.view.LinearLayout,P=android.view.FrameLayout,R=10 5 60 40\n" + + "{V=3,N=android.view.LinearLayout,P=android.view.FrameLayout,R=10 5 60 40,Q=0 0 320 480\n" + "@name:uri=value\n" + "@second-name:my-uri=my = value \n" + - "{V=2,N=android.view.Button,P=android.view.LinearLayout,R=10 20 30 40\n" + + "{V=3,N=android.view.Button,P=android.view.LinearLayout,R=10 20 30 40,Q=0 0 320 480\n" + "@name1:uri1=value1\n" + "}\n" + - "{V=2,N=android.view.CheckBox,P=android.view.LinearLayout,R=-1 -2 -3 -4\n" + + "{V=3,N=android.view.CheckBox,P=android.view.LinearLayout,R=-1 -2 -3 -4,Q=-1 -2 -3 -4\n" + "@name2:uri2=value2\n" + "@name3:uri3=value3\n" + "}\n" + @@ -187,21 +198,21 @@ public class SimpleElementTest extends TestCase { assertArrayEquals( new SimpleElement[] { e, e2, e3 }, SimpleElement.parseString( - "{V=2,N=android.view.LinearLayout,P=android.view.FrameLayout,R=10 5 60 40\n" + + "{V=3,N=android.view.LinearLayout,P=android.view.FrameLayout,R=10 5 60 40,Q=0 0 320 480\n" + "@name:uri=value\n" + "@second-name:my-uri=my = value \n" + - "{V=2,N=android.view.Button,P=android.view.LinearLayout,R=10 20 30 40\n" + + "{V=3,N=android.view.Button,P=android.view.LinearLayout,R=10 20 30 40,Q=0 0 320 480\n" + "@name1:uri1=value1\n" + "}\n" + - "{V=2,N=android.view.CheckBox,P=android.view.LinearLayout,R=-1 -2 -3 -4\n" + + "{V=3,N=android.view.CheckBox,P=android.view.LinearLayout,R=-1 -2 -3 -4\n" + "@name2:uri2=value2\n" + "@name3:uri3=value3\n" + "}\n" + "}\n" + - "{V=2,N=android.view.Button,P=android.view.LinearLayout,R=10 20 30 40\n" + + "{V=3,N=android.view.Button,P=android.view.LinearLayout,R=10 20 30 40,Q=0 0 320 480\n" + "@name1:uri1=value1\n" + "}\n" + - "{V=2,N=android.view.CheckBox,P=android.view.LinearLayout,R=-1 -2 -3 -4\n" + + "{V=3,N=android.view.CheckBox,P=android.view.LinearLayout,R=-1 -2 -3 -4,Q=-1 -2 -3 -4\n" + "@name2:uri2=value2\n" + "@name3:uri3=value3\n" + "}\n")); @@ -239,16 +250,16 @@ public class SimpleElementTest extends TestCase { new SimpleElement[] {}, e.getInnerElements()); - e.addInnerElement(new SimpleElement("android.view.Button", null, null)); + e.addInnerElement(new SimpleElement("android.view.Button", null, null, null)); assertArrayEquals( - new SimpleElement[] { new SimpleElement("android.view.Button", null, null) }, + new SimpleElement[] { new SimpleElement("android.view.Button", null, null, null) }, e.getInnerElements()); - e.addInnerElement(new SimpleElement("android.view.CheckBox", null, null)); + e.addInnerElement(new SimpleElement("android.view.CheckBox", null, null, null)); assertArrayEquals( - new SimpleElement[] { new SimpleElement("android.view.Button", null, null), - new SimpleElement("android.view.CheckBox", null, null) }, - e.getInnerElements()); + new SimpleElement[] { new SimpleElement("android.view.Button", null, null, null), + new SimpleElement("android.view.CheckBox", null, null, null) }, + e.getInnerElements()); } public final void testEqualsObject() { @@ -257,30 +268,42 @@ public class SimpleElementTest extends TestCase { assertNotSame(new SimpleElement("android.view.LinearLayout", "android.view.FrameLayout", - new Rect(10, 5, 60, 40)), + new Rect(10, 5, 60, 40), + new Rect(0, 0, 320, 480)), e); assertEquals(new SimpleElement("android.view.LinearLayout", "android.view.FrameLayout", - new Rect(10, 5, 60, 40)), + new Rect(10, 5, 60, 40), + new Rect(0, 0, 320, 480)), e); assertTrue(e.equals(new SimpleElement("android.view.LinearLayout", "android.view.FrameLayout", - new Rect(10, 5, 60, 40)))); + new Rect(10, 5, 60, 40), + new Rect(0, 0, 320, 480)))); // not the same FQCN assertFalse(e.equals(new SimpleElement("android.view.Button", "android.view.FrameLayout", - new Rect(10, 5, 60, 40)))); + new Rect(10, 5, 60, 40), + new Rect(0, 0, 320, 480)))); // not the same parent assertFalse(e.equals(new SimpleElement("android.view.LinearLayout", "android.view.LinearLayout", - new Rect(10, 5, 60, 40)))); + new Rect(10, 5, 60, 40), + new Rect(0, 0, 320, 480)))); // not the same bounds assertFalse(e.equals(new SimpleElement("android.view.LinearLayout", - "android.view.LinearLayout", - new Rect(10, 25, 30, 40)))); + "android.view.FrameLayout", + new Rect(10, 25, 30, 40), + new Rect(0, 0, 320, 480)))); + + // not the same parent bounds + assertFalse(e.equals(new SimpleElement("android.view.LinearLayout", + "android.view.FrameLayout", + new Rect(10, 5, 60, 40), + new Rect(10, 100, 160, 240)))); } public final void testHashCode() { @@ -288,23 +311,33 @@ public class SimpleElementTest extends TestCase { assertEquals(he, new SimpleElement("android.view.LinearLayout", "android.view.FrameLayout", - new Rect(10, 5, 60, 40)).hashCode()); + new Rect(10, 5, 60, 40), + new Rect(0, 0, 320, 480)).hashCode()); // not the same FQCN assertFalse(he == new SimpleElement("android.view.Button", "android.view.FrameLayout", - new Rect(10, 5, 60, 40)).hashCode()); + new Rect(10, 5, 60, 40), + new Rect(0, 0, 320, 480)).hashCode()); // not the same parent assertFalse(he == new SimpleElement("android.view.LinearLayout", "android.view.Button", - new Rect(10, 5, 60, 40)).hashCode()); + new Rect(10, 5, 60, 40), + new Rect(0, 0, 320, 480)).hashCode()); // not the same bounds assertFalse(he == new SimpleElement("android.view.LinearLayout", - "android.view.LinearLayout", - new Rect(10, 25, 30, 40)).hashCode()); + "android.view.FrameLayout", + new Rect(10, 25, 30, 40), + new Rect(0, 0, 320, 480)).hashCode()); + + // not the same parent bounds + assertFalse(he == new SimpleElement("android.view.LinearLayout", + "android.view.FrameLayout", + new Rect(10, 25, 30, 40), + new Rect(10, 100, 160, 240)).hashCode()); } } |