Home | History | Annotate | Download | only in gscripts
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.adt.gscripts;
     18 
     19 public class BaseLayout extends BaseView {
     20 
     21     public boolean onInitialize(String fqcn) {
     22         return super.onInitialize(fqcn);
     23     }
     24 
     25     public void onDispose() {
     26         super.onDispose();
     27     }
     28 
     29     // ==== Utility methods used by derived layouts ====
     30 
     31     // TODO revisit.
     32     protected String[] getLayoutAttrFilter() {
     33         return [
     34             // from AbsoluteLayout
     35             "layout_x",
     36             "layout_y",
     37 
     38             // from RelativeLayout
     39             "layout_above",
     40             "layout_below",
     41             "layout_toLeftOf",
     42             "layout_toRightOf",
     43             "layout_alignBaseline",
     44             "layout_alignTop",
     45             "layout_alignBottom",
     46             "layout_alignLeft",
     47             "layout_alignRight",
     48             "layout_alignParentTop",
     49             "layout_alignParentBottom",
     50             "layout_alignParentLeft",
     51             "layout_alignParentRight",
     52             "layout_alignWithParentMissing",
     53             "layout_centerHorizontal",
     54             "layout_centerInParent",
     55             "layout_centerVertical",
     56         ];
     57     }
     58 
     59     /**
     60      * Draws the bounds of the given elements and all its children elements
     61      * in the canvas with the specified offet.
     62      */
     63     protected void drawElement(IGraphics gc, IDragElement element, int offsetX, int offsetY) {
     64         Rect b = element.getBounds();
     65         if (b.isValid()) {
     66             b = b.copy().offsetBy(offsetX, offsetY);
     67             gc.drawRect(b);
     68         }
     69 
     70         for(inner in element.getInnerElements()) {
     71             drawElement(gc, inner, offsetX, offsetY);
     72         }
     73     }
     74 
     75     /**
     76      * Collect all the "android:id" IDs from the dropped elements.
     77      *
     78      * When moving objects within the same canvas, that's all there is to do.
     79      * However if the objects are moved to a different canvas or are copied
     80      * then set createNewIds to true to find the existing IDs under targetNode
     81      * and create a map with new non-conflicting unique IDs as needed.
     82      *
     83      * Returns a map String old-id => tuple (String new-id, String fqcn)
     84      * where fqcn is the FQCN of the element.
     85      */
     86     protected Map getDropIdMap(INode targetNode,
     87                                IDragElement[] elements,
     88                                boolean createNewIds) {
     89         def idMap = [:];
     90 
     91         if (createNewIds) {
     92             collectIds(idMap, elements);
     93             // Need to remap ids if necessary
     94             idMap = remapIds(targetNode, idMap);
     95         }
     96 
     97         return idMap;
     98     }
     99 
    100 
    101     /**
    102      * Fills idMap with a map String id => tuple (String id, String fqcn)
    103      * where fqcn is the FQCN of the element (in case we want to generate
    104      * new IDs based on the element type.)
    105      *
    106      * @see #getDropIdMap
    107      */
    108     protected Map collectIds(Map idMap, IDragElement[] elements) {
    109         for (element in elements) {
    110             def attr = element.getAttribute(ANDROID_URI, ATTR_ID);
    111             if (attr != null) {
    112                 String id = attr.getValue();
    113                 if (id != null && id != "") {
    114                     idMap.put(id, [id, element.getFqcn()]);
    115                 }
    116             }
    117 
    118             collectIds(idMap, element.getInnerElements());
    119         }
    120 
    121         return idMap;
    122     }
    123 
    124     /**
    125      * Used by #getDropIdMap to find new IDs in case of conflict.
    126      */
    127     protected Map remapIds(INode node, Map idMap) {
    128         // Visit the document to get a list of existing ids
    129         def existingIdMap = [:];
    130         collectExistingIds(node.getRoot(), existingIdMap);
    131 
    132         def new_map = [:];
    133         idMap.each() { key, value ->
    134             def id = normalizeId(key);
    135 
    136             if (!existingIdMap.containsKey(id)) {
    137                 // Not a conflict. Use as-is.
    138                 new_map.put(key, value);
    139                 if (key != id) {
    140                     new_map.put(id, value);
    141                 }
    142             } else {
    143                 // There is a conflict. Get a new id.
    144                 def new_id = findNewId(value[1], existingIdMap);
    145                 value[0] = new_id;
    146                 new_map.put(id, value);
    147                 new_map.put(id.replaceFirst("@\\+", "@"), value);
    148             }
    149         }
    150 
    151         return new_map;
    152     }
    153 
    154     /**
    155      * Used by #remapIds to find a new ID for a conflicting element.
    156      */
    157     protected String findNewId(String fqcn, Map existingIdMap) {
    158         // Get the last component of the FQCN (e.g. "android.view.Button" => "Button")
    159         String name = fqcn[fqcn.lastIndexOf(".")+1 .. fqcn.length()-1];
    160 
    161         for (int i = 1; i < 1000000; i++) {
    162             String id = String.format("@+id/%s%02d", name, i);
    163             if (!existingIdMap.containsKey(id)) {
    164                 existingIdMap.put(id, id);
    165                 return id;
    166             }
    167         }
    168 
    169         // We'll never reach here.
    170         return null;
    171     }
    172 
    173     /**
    174      * Used by #getDropIdMap to find existing IDs recursively.
    175      */
    176     protected void collectExistingIds(INode root, Map existingIdMap) {
    177         if (root == null) {
    178             return;
    179         }
    180 
    181         def id = root.getStringAttr(ANDROID_URI, ATTR_ID);
    182         if (id != null) {
    183             id = normalizeId(id);
    184 
    185             if (!existingIdMap.containsKey(id)) {
    186                 existingIdMap.put(id, id);
    187             }
    188         }
    189 
    190         for(child in root.getChildren()) {
    191             collectExistingIds(child, existingIdMap);
    192         }
    193     }
    194 
    195     /**
    196      * Transforms @id/name into @+id/name to treat both forms the same way.
    197      */
    198     protected String normalizeId(String id) {
    199         if (id.indexOf("@+") == -1) {
    200             id = id.replaceFirst("@", "@+");
    201         }
    202         return id;
    203     }
    204 
    205     /**
    206      * Copies all the attributes from oldElement to newNode.
    207      *
    208      * Uses the idMap to transform the value of all attributes of Format.REFERENCE,
    209      * If filter is non-null, it's a closure that takes for argument:
    210      *   String attribue-uri (namespace), String attribute-name, String attribute-value
    211      * The closure should return a valid replacement string.
    212      * The closure can return either null, false or an empty string to prevent the attribute
    213      * from being copied into the new node.
    214      */
    215     protected void addAttributes(INode newNode, IDragElement oldElement,
    216                                  Map idMap, Closure filter) {
    217 
    218         // A little trick here: when creating new UI widgets by dropping them from
    219         // the palette, we assign them a new id and then set the text attribute
    220         // to that id, so for example a Button will have android:text="@+id/Button01".
    221         // Here we detect if such an id is being remapped to a new id and if there's
    222         // a text attribute with exactly the same id name, we update it too.
    223         String oldText = null;
    224         String oldId = null;
    225         String newId = null;
    226 
    227         for (attr in oldElement.getAttributes()) {
    228             String uri = attr.getUri();
    229             String name = attr.getName();
    230             String value = attr.getValue();
    231 
    232             if (uri == ANDROID_URI) {
    233                 if (name == ATTR_ID) {
    234                     oldId = value;
    235                 } else if (name == ATTR_TEXT) {
    236                     oldText = value;
    237                 }
    238             }
    239 
    240             def attrInfo = newNode.getAttributeInfo(uri, name);
    241             if (attrInfo != null) {
    242                 def formats = attrInfo.getFormats();
    243                 if (formats != null && IAttributeInfo.Format.REFERENCE in formats) {
    244                     if (idMap.containsKey(value)) {
    245                         value = idMap[value][0];
    246                     }
    247                 }
    248             }
    249 
    250             if (filter != null) {
    251                 value = filter(uri, name, value);
    252             }
    253             if (value != null && value != false && value != "") {
    254                 newNode.setAttribute(uri, name, value);
    255 
    256                 if (uri == ANDROID_URI && name == ATTR_ID && oldId != null && value != oldId) {
    257                     newId = value;
    258                 }
    259             }
    260         }
    261 
    262         if (newId != null && oldText == oldId) {
    263             newNode.setAttribute(ANDROID_URI, ATTR_TEXT, newId);
    264         }
    265     }
    266 
    267     /**
    268      * Adds all the children elements of oldElement to newNode, recursively.
    269      * Attributes are adjusted by calling addAttributes with idMap as necessary, with
    270      * no closure filter.
    271      */
    272     protected void addInnerElements(INode newNode, IDragElement oldElement, Map idMap) {
    273 
    274         for (element in oldElement.getInnerElements()) {
    275             String fqcn = element.getFqcn();
    276             INode childNode = newNode.appendChild(fqcn);
    277 
    278             addAttributes(childNode, element, idMap, null /* closure */);
    279             addInnerElements(childNode, element, idMap);
    280         }
    281     }
    282 
    283 }
    284