Home | History | Annotate | Download | only in layout
      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.ide.common.layout;
     18 
     19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
     20 import static com.android.ide.common.layout.LayoutConstants.ATTR_BASELINE_ALIGNED;
     21 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_GRAVITY;
     22 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT;
     23 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WEIGHT;
     24 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH;
     25 import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION;
     26 import static com.android.ide.common.layout.LayoutConstants.ATTR_WEIGHT_SUM;
     27 import static com.android.ide.common.layout.LayoutConstants.VALUE_1;
     28 import static com.android.ide.common.layout.LayoutConstants.VALUE_HORIZONTAL;
     29 import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL;
     30 import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT;
     31 import static com.android.ide.common.layout.LayoutConstants.VALUE_ZERO_DP;
     32 
     33 import com.android.annotations.VisibleForTesting;
     34 import com.android.ide.common.api.DrawingStyle;
     35 import com.android.ide.common.api.DropFeedback;
     36 import com.android.ide.common.api.IClientRulesEngine;
     37 import com.android.ide.common.api.IDragElement;
     38 import com.android.ide.common.api.IFeedbackPainter;
     39 import com.android.ide.common.api.IGraphics;
     40 import com.android.ide.common.api.IMenuCallback;
     41 import com.android.ide.common.api.INode;
     42 import com.android.ide.common.api.INodeHandler;
     43 import com.android.ide.common.api.IViewMetadata;
     44 import com.android.ide.common.api.IViewMetadata.FillPreference;
     45 import com.android.ide.common.api.IViewRule;
     46 import com.android.ide.common.api.InsertType;
     47 import com.android.ide.common.api.Point;
     48 import com.android.ide.common.api.Rect;
     49 import com.android.ide.common.api.RuleAction;
     50 import com.android.ide.common.api.RuleAction.Choices;
     51 import com.android.ide.common.api.SegmentType;
     52 import com.android.ide.eclipse.adt.AdtPlugin;
     53 import com.android.sdklib.SdkConstants;
     54 
     55 import java.net.URL;
     56 import java.util.ArrayList;
     57 import java.util.Arrays;
     58 import java.util.Collections;
     59 import java.util.List;
     60 import java.util.Locale;
     61 import java.util.Map;
     62 
     63 /**
     64  * An {@link IViewRule} for android.widget.LinearLayout and all its derived
     65  * classes.
     66  */
     67 public class LinearLayoutRule extends BaseLayoutRule {
     68     private static final String ACTION_ORIENTATION = "_orientation"; //$NON-NLS-1$
     69     private static final String ACTION_WEIGHT = "_weight"; //$NON-NLS-1$
     70     private static final String ACTION_DISTRIBUTE = "_distribute"; //$NON-NLS-1$
     71     private static final String ACTION_BASELINE = "_baseline"; //$NON-NLS-1$
     72     private static final String ACTION_CLEAR = "_clear"; //$NON-NLS-1$
     73     private static final String ACTION_DOMINATE = "_dominate"; //$NON-NLS-1$
     74 
     75     private static final URL ICON_HORIZONTAL =
     76         LinearLayoutRule.class.getResource("hlinear.png"); //$NON-NLS-1$
     77     private static final URL ICON_VERTICAL =
     78         LinearLayoutRule.class.getResource("vlinear.png"); //$NON-NLS-1$
     79     private static final URL ICON_WEIGHTS =
     80         LinearLayoutRule.class.getResource("weights.png"); //$NON-NLS-1$
     81     private static final URL ICON_DISTRIBUTE =
     82         LinearLayoutRule.class.getResource("distribute.png"); //$NON-NLS-1$
     83     private static final URL ICON_BASELINE =
     84         LinearLayoutRule.class.getResource("baseline.png"); //$NON-NLS-1$
     85     private static final URL ICON_CLEAR_WEIGHTS =
     86             LinearLayoutRule.class.getResource("clearweights.png"); //$NON-NLS-1$
     87     private static final URL ICON_DOMINATE =
     88             LinearLayoutRule.class.getResource("allweight.png"); //$NON-NLS-1$
     89 
     90     /**
     91      * Returns the current orientation, regardless of whether it has been defined in XML
     92      *
     93      * @param node The LinearLayout to look up the orientation for
     94      * @return "horizontal" or "vertical" depending on the current orientation of the
     95      *         linear layout
     96      */
     97     private String getCurrentOrientation(final INode node) {
     98         String orientation = node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION);
     99         if (orientation == null || orientation.length() == 0) {
    100             orientation = VALUE_HORIZONTAL;
    101         }
    102         return orientation;
    103     }
    104 
    105     /**
    106      * Returns true if the given node represents a vertical linear layout.
    107      * @param node the node to check layout orientation for
    108      * @return true if the layout is in vertical mode, otherwise false
    109      */
    110     protected boolean isVertical(INode node) {
    111         // Horizontal is the default, so if no value is specified it is horizontal.
    112         return VALUE_VERTICAL.equals(node.getStringAttr(ANDROID_URI,
    113                 ATTR_ORIENTATION));
    114     }
    115 
    116     /**
    117      * Returns true if this LinearLayout supports switching orientation.
    118      *
    119      * @return true if this layout supports orientations
    120      */
    121     protected boolean supportsOrientation() {
    122         return true;
    123     }
    124 
    125     @Override
    126     public void addLayoutActions(List<RuleAction> actions, final INode parentNode,
    127             final List<? extends INode> children) {
    128         super.addLayoutActions(actions, parentNode, children);
    129         if (supportsOrientation()) {
    130             Choices action = RuleAction.createChoices(
    131                     ACTION_ORIENTATION, "Orientation",  //$NON-NLS-1$
    132                     new PropertyCallback(Collections.singletonList(parentNode),
    133                             "Change LinearLayout Orientation",
    134                             ANDROID_URI, ATTR_ORIENTATION),
    135                     Arrays.<String>asList("Set Horizontal Orientation","Set Vertical Orientation"),
    136                     Arrays.<URL>asList(ICON_HORIZONTAL, ICON_VERTICAL),
    137                     Arrays.<String>asList("horizontal", "vertical"),
    138                     getCurrentOrientation(parentNode),
    139                     null /* icon */,
    140                     -10,
    141                     false /* supportsMultipleNodes */
    142             );
    143             action.setRadio(true);
    144             actions.add(action);
    145         }
    146         if (!isVertical(parentNode)) {
    147             String current = parentNode.getStringAttr(ANDROID_URI, ATTR_BASELINE_ALIGNED);
    148             boolean isAligned =  current == null || Boolean.valueOf(current);
    149             actions.add(RuleAction.createToggle(null, "Toggle Baseline Alignment",
    150                     isAligned,
    151                     new PropertyCallback(Collections.singletonList(parentNode),
    152                             "Change Baseline Alignment",
    153                             ANDROID_URI, ATTR_BASELINE_ALIGNED), // TODO: Also set index?
    154                     ICON_BASELINE, 38, false));
    155         }
    156 
    157         // Gravity
    158         if (children != null && children.size() > 0) {
    159             actions.add(RuleAction.createSeparator(35));
    160 
    161             // Margins
    162             actions.add(createMarginAction(parentNode, children));
    163 
    164             // Gravity
    165             actions.add(createGravityAction(children, ATTR_LAYOUT_GRAVITY));
    166 
    167             // Weights
    168             IMenuCallback actionCallback = new IMenuCallback() {
    169                 public void action(final RuleAction action, List<? extends INode> selectedNodes,
    170                         final String valueId, final Boolean newValue) {
    171                     parentNode.editXml("Change Weight", new INodeHandler() {
    172                         public void handle(INode n) {
    173                             String id = action.getId();
    174                             if (id.equals(ACTION_WEIGHT)) {
    175                                 String weight =
    176                                     children.get(0).getStringAttr(ANDROID_URI, ATTR_LAYOUT_WEIGHT);
    177                                 if (weight == null || weight.length() == 0) {
    178                                     weight = "0.0"; //$NON-NLS-1$
    179                                 }
    180                                 weight = mRulesEngine.displayInput("Enter Weight Value:", weight,
    181                                         null);
    182                                 if (weight != null) {
    183                                     for (INode child : children) {
    184                                         child.setAttribute(ANDROID_URI,
    185                                                 ATTR_LAYOUT_WEIGHT, weight);
    186                                     }
    187                                 }
    188                             } else if (id.equals(ACTION_DISTRIBUTE)) {
    189                                 distributeWeights(parentNode, parentNode.getChildren());
    190                             } else if (id.equals(ACTION_CLEAR)) {
    191                                 clearWeights(parentNode);
    192                             } else if (id.equals(ACTION_CLEAR) || id.equals(ACTION_DOMINATE)) {
    193                                 clearWeights(parentNode);
    194                                 distributeWeights(parentNode,
    195                                         children.toArray(new INode[children.size()]));
    196                             } else {
    197                                 assert id.equals(ACTION_BASELINE);
    198                             }
    199                         }
    200                     });
    201                 }
    202             };
    203             actions.add(RuleAction.createSeparator(50));
    204             actions.add(RuleAction.createAction(ACTION_DISTRIBUTE, "Distribute Weights Evenly",
    205                     actionCallback, ICON_DISTRIBUTE, 60, false /*supportsMultipleNodes*/));
    206             actions.add(RuleAction.createAction(ACTION_DOMINATE, "Assign All Weight",
    207                     actionCallback, ICON_DOMINATE, 70, false));
    208             actions.add(RuleAction.createAction(ACTION_WEIGHT, "Change Layout Weight",
    209                     actionCallback, ICON_WEIGHTS, 80, false));
    210             actions.add(RuleAction.createAction(ACTION_CLEAR, "Clear All Weights",
    211                      actionCallback, ICON_CLEAR_WEIGHTS, 90, false));
    212         }
    213     }
    214 
    215     private void distributeWeights(INode parentNode, INode[] targets) {
    216         // Any XML to get weight sum?
    217         String weightSum = parentNode.getStringAttr(ANDROID_URI,
    218                 ATTR_WEIGHT_SUM);
    219         double sum = -1.0;
    220         if (weightSum != null) {
    221             // Distribute
    222             try {
    223                 sum = Double.parseDouble(weightSum);
    224             } catch (NumberFormatException nfe) {
    225                 // Just keep using the default
    226             }
    227         }
    228         int numTargets = targets.length;
    229         double share;
    230         if (sum <= 0.0) {
    231             // The sum will be computed from the children, so just
    232             // use arbitrary amount
    233             share = 1.0;
    234         } else {
    235             share = sum / numTargets;
    236         }
    237         String value = formatFloatAttribute((float) share);
    238         String sizeAttribute = isVertical(parentNode) ?
    239                 ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH;
    240         for (INode target : targets) {
    241             target.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, value);
    242             // Also set the width/height to 0dp to ensure actual equal
    243             // size (without this, only the remaining space is
    244             // distributed)
    245             if (VALUE_WRAP_CONTENT.equals(target.getStringAttr(ANDROID_URI, sizeAttribute))) {
    246                 target.setAttribute(ANDROID_URI, sizeAttribute, VALUE_ZERO_DP);
    247             }
    248         }
    249     }
    250 
    251     private void clearWeights(INode parentNode) {
    252         // Clear attributes
    253         String sizeAttribute = isVertical(parentNode)
    254                 ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH;
    255         for (INode target : parentNode.getChildren()) {
    256             target.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null);
    257             String size = target.getStringAttr(ANDROID_URI, sizeAttribute);
    258             if (size != null && size.startsWith("0")) { //$NON-NLS-1$
    259                 target.setAttribute(ANDROID_URI, sizeAttribute, VALUE_WRAP_CONTENT);
    260             }
    261         }
    262     }
    263 
    264     // ==== Drag'n'drop support ====
    265 
    266     @Override
    267     public DropFeedback onDropEnter(final INode targetNode, Object targetView,
    268             final IDragElement[] elements) {
    269 
    270         if (elements.length == 0) {
    271             return null;
    272         }
    273 
    274         Rect bn = targetNode.getBounds();
    275         if (!bn.isValid()) {
    276             return null;
    277         }
    278 
    279         boolean isVertical = isVertical(targetNode);
    280 
    281         // Prepare a list of insertion points: X coords for horizontal, Y for
    282         // vertical.
    283         List<MatchPos> indexes = new ArrayList<MatchPos>();
    284 
    285         int last = isVertical ? bn.y : bn.x;
    286         int pos = 0;
    287         boolean lastDragged = false;
    288         int selfPos = -1;
    289         for (INode it : targetNode.getChildren()) {
    290             Rect bc = it.getBounds();
    291             if (bc.isValid()) {
    292                 // First see if this node looks like it's the same as one of the
    293                 // *dragged* bounds
    294                 boolean isDragged = false;
    295                 for (IDragElement element : elements) {
    296                     // This tries to determine if an INode corresponds to an
    297                     // IDragElement, by comparing their bounds.
    298                     if (bc.equals(element.getBounds())) {
    299                         isDragged = true;
    300                     }
    301                 }
    302 
    303                 // We don't want to insert drag positions before or after the
    304                 // element that is itself being dragged. However, we -do- want
    305                 // to insert a match position here, at the center, such that
    306                 // when you drag near its current position we show a match right
    307                 // where it's already positioned.
    308                 if (isDragged) {
    309                     int v = isVertical ? bc.y + (bc.h / 2) : bc.x + (bc.w / 2);
    310                     selfPos = pos;
    311                     indexes.add(new MatchPos(v, pos++));
    312                 } else if (lastDragged) {
    313                     // Even though we don't want to insert a match below, we
    314                     // need to increment the index counter such that subsequent
    315                     // lines know their correct index in the child list.
    316                     pos++;
    317                 } else {
    318                     // Add an insertion point between the last point and the
    319                     // start of this child
    320                     int v = isVertical ? bc.y : bc.x;
    321                     v = (last + v) / 2;
    322                     indexes.add(new MatchPos(v, pos++));
    323                 }
    324 
    325                 last = isVertical ? (bc.y + bc.h) : (bc.x + bc.w);
    326                 lastDragged = isDragged;
    327             } else {
    328                 // We still have to count this position even if it has no bounds, or
    329                 // subsequent children will be inserted at the wrong place
    330                 pos++;
    331             }
    332         }
    333 
    334         // Finally add an insert position after all the children - unless of
    335         // course we happened to be dragging the last element
    336         if (!lastDragged) {
    337             int v = last + 1;
    338             indexes.add(new MatchPos(v, pos));
    339         }
    340 
    341         int posCount = targetNode.getChildren().length + 1;
    342         return new DropFeedback(new LinearDropData(indexes, posCount, isVertical, selfPos),
    343                 new IFeedbackPainter() {
    344 
    345                     public void paint(IGraphics gc, INode node, DropFeedback feedback) {
    346                         // Paint callback for the LinearLayout. This is called
    347                         // by the canvas when a draw is needed.
    348                         drawFeedback(gc, node, elements, feedback);
    349                     }
    350                 });
    351     }
    352 
    353     void drawFeedback(IGraphics gc, INode node, IDragElement[] elements, DropFeedback feedback) {
    354         Rect b = node.getBounds();
    355         if (!b.isValid()) {
    356             return;
    357         }
    358 
    359         // Highlight the receiver
    360         gc.useStyle(DrawingStyle.DROP_RECIPIENT);
    361         gc.drawRect(b);
    362 
    363         gc.useStyle(DrawingStyle.DROP_ZONE);
    364 
    365         LinearDropData data = (LinearDropData) feedback.userData;
    366         boolean isVertical = data.isVertical();
    367         int selfPos = data.getSelfPos();
    368 
    369         for (MatchPos it : data.getIndexes()) {
    370             int i = it.getDistance();
    371             int pos = it.getPosition();
    372             // Don't show insert drop zones for "self"-index since that one goes
    373             // right through the center of the widget rather than in a sibling
    374             // position
    375             if (pos != selfPos) {
    376                 if (isVertical) {
    377                     // draw horizontal lines
    378                     gc.drawLine(b.x, i, b.x + b.w, i);
    379                 } else {
    380                     // draw vertical lines
    381                     gc.drawLine(i, b.y, i, b.y + b.h);
    382                 }
    383             }
    384         }
    385 
    386         Integer currX = data.getCurrX();
    387         Integer currY = data.getCurrY();
    388 
    389         if (currX != null && currY != null) {
    390             gc.useStyle(DrawingStyle.DROP_ZONE_ACTIVE);
    391 
    392             int x = currX;
    393             int y = currY;
    394 
    395             Rect be = elements[0].getBounds();
    396 
    397             // Draw a clear line at the closest drop zone (unless we're over the
    398             // dragged element itself)
    399             if (data.getInsertPos() != selfPos || selfPos == -1) {
    400                 gc.useStyle(DrawingStyle.DROP_PREVIEW);
    401                 if (data.getWidth() != null) {
    402                     int width = data.getWidth();
    403                     int fromX = x - width / 2;
    404                     int toX = x + width / 2;
    405                     gc.drawLine(fromX, y, toX, y);
    406                 } else if (data.getHeight() != null) {
    407                     int height = data.getHeight();
    408                     int fromY = y - height / 2;
    409                     int toY = y + height / 2;
    410                     gc.drawLine(x, fromY, x, toY);
    411                 }
    412             }
    413 
    414             if (be.isValid()) {
    415                 boolean isLast = data.isLastPosition();
    416 
    417                 // At least the first element has a bound. Draw rectangles for
    418                 // all dropped elements with valid bounds, offset at the drop
    419                 // point.
    420                 int offsetX;
    421                 int offsetY;
    422                 if (isVertical) {
    423                     offsetX = b.x - be.x;
    424                     offsetY = currY - be.y - (isLast ? 0 : (be.h / 2));
    425 
    426                 } else {
    427                     offsetX = currX - be.x - (isLast ? 0 : (be.w / 2));
    428                     offsetY = b.y - be.y;
    429                 }
    430 
    431                 gc.useStyle(DrawingStyle.DROP_PREVIEW);
    432                 for (IDragElement element : elements) {
    433                     Rect bounds = element.getBounds();
    434                     if (bounds.isValid() && (bounds.w > b.w || bounds.h > b.h) &&
    435                             node.getChildren().length == 0) {
    436                         // The bounds of the child does not fully fit inside the target.
    437                         // Limit the bounds to the layout bounds (but only when there
    438                         // are no children, since otherwise positioning around the existing
    439                         // children gets difficult)
    440                         final int px, py, pw, ph;
    441                         if (bounds.w > b.w) {
    442                             px = b.x;
    443                             pw = b.w;
    444                         } else {
    445                             px = bounds.x + offsetX;
    446                             pw = bounds.w;
    447                         }
    448                         if (bounds.h > b.h) {
    449                             py = b.y;
    450                             ph = b.h;
    451                         } else {
    452                             py = bounds.y + offsetY;
    453                             ph = bounds.h;
    454                         }
    455                         Rect within = new Rect(px, py, pw, ph);
    456                         gc.drawRect(within);
    457                     } else {
    458                         drawElement(gc, element, offsetX, offsetY);
    459                     }
    460                 }
    461             }
    462         }
    463     }
    464 
    465     @Override
    466     public DropFeedback onDropMove(INode targetNode, IDragElement[] elements,
    467             DropFeedback feedback, Point p) {
    468         Rect b = targetNode.getBounds();
    469         if (!b.isValid()) {
    470             return feedback;
    471         }
    472 
    473         LinearDropData data = (LinearDropData) feedback.userData;
    474         boolean isVertical = data.isVertical();
    475 
    476         int bestDist = Integer.MAX_VALUE;
    477         int bestIndex = Integer.MIN_VALUE;
    478         Integer bestPos = null;
    479 
    480         for (MatchPos index : data.getIndexes()) {
    481             int i = index.getDistance();
    482             int pos = index.getPosition();
    483             int dist = (isVertical ? p.y : p.x) - i;
    484             if (dist < 0)
    485                 dist = -dist;
    486             if (dist < bestDist) {
    487                 bestDist = dist;
    488                 bestIndex = i;
    489                 bestPos = pos;
    490                 if (bestDist <= 0)
    491                     break;
    492             }
    493         }
    494 
    495         if (bestIndex != Integer.MIN_VALUE) {
    496             Integer oldX = data.getCurrX();
    497             Integer oldY = data.getCurrY();
    498 
    499             if (isVertical) {
    500                 data.setCurrX(b.x + b.w / 2);
    501                 data.setCurrY(bestIndex);
    502                 data.setWidth(b.w);
    503                 data.setHeight(null);
    504             } else {
    505                 data.setCurrX(bestIndex);
    506                 data.setCurrY(b.y + b.h / 2);
    507                 data.setWidth(null);
    508                 data.setHeight(b.h);
    509             }
    510 
    511             data.setInsertPos(bestPos);
    512 
    513             feedback.requestPaint = !equals(oldX, data.getCurrX())
    514                     || !equals(oldY, data.getCurrY());
    515         }
    516 
    517         return feedback;
    518     }
    519 
    520     private static boolean equals(Integer i1, Integer i2) {
    521         if (i1 == i2) {
    522             return true;
    523         } else if (i1 != null) {
    524             return i1.equals(i2);
    525         } else {
    526             // We know i2 != null
    527             return i2.equals(i1);
    528         }
    529     }
    530 
    531     @Override
    532     public void onDropLeave(INode targetNode, IDragElement[] elements, DropFeedback feedback) {
    533         // ignore
    534     }
    535 
    536     @Override
    537     public void onDropped(final INode targetNode, final IDragElement[] elements,
    538             final DropFeedback feedback, final Point p) {
    539 
    540         LinearDropData data = (LinearDropData) feedback.userData;
    541         final int initialInsertPos = data.getInsertPos();
    542         insertAt(targetNode, elements, feedback.isCopy || !feedback.sameCanvas, initialInsertPos);
    543     }
    544 
    545     @Override
    546     public void onChildInserted(INode node, INode parent, InsertType insertType) {
    547         if (insertType == InsertType.MOVE_WITHIN) {
    548             // Don't adjust widths/heights/weights when just moving within a single
    549             // LinearLayout
    550             return;
    551         }
    552 
    553         // Attempt to set fill-properties on newly added views such that for example,
    554         // in a vertical layout, a text field defaults to filling horizontally, but not
    555         // vertically.
    556         String fqcn = node.getFqcn();
    557         IViewMetadata metadata = mRulesEngine.getMetadata(fqcn);
    558         if (metadata != null) {
    559             boolean vertical = isVertical(parent);
    560             FillPreference fill = metadata.getFillPreference();
    561             String fillParent = getFillParentValueName();
    562             if (fill.fillHorizontally(vertical)) {
    563                 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, fillParent);
    564             } else if (!vertical && fill == FillPreference.WIDTH_IN_VERTICAL) {
    565                 // In a horizontal layout, make views that would fill horizontally in a
    566                 // vertical layout have a non-zero weight instead. This will make the item
    567                 // fill but only enough to allow other views to be shown as well.
    568                 // (However, for drags within the same layout we do not touch
    569                 // the weight, since it might already have been tweaked to a particular
    570                 // value)
    571                 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, VALUE_1);
    572             }
    573             if (fill.fillVertically(vertical)) {
    574                 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, fillParent);
    575             }
    576         }
    577 
    578         // If you insert into a layout that already is using layout weights,
    579         // and all the layout weights are the same (nonzero) value, then use
    580         // the same weight for this new layout as well. Also duplicate the 0dip/0px/0dp
    581         // sizes, if used.
    582         boolean duplicateWeight = true;
    583         boolean duplicate0dip = true;
    584         String sameWeight = null;
    585         String sizeAttribute = isVertical(parent) ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH;
    586         for (INode target : parent.getChildren()) {
    587             if (target == node) {
    588                 continue;
    589             }
    590             String weight = target.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WEIGHT);
    591             if (weight == null || weight.length() == 0) {
    592                 duplicateWeight = false;
    593                 break;
    594             } else if (sameWeight != null && !sameWeight.equals(weight)) {
    595                 duplicateWeight = false;
    596             } else {
    597                 sameWeight = weight;
    598             }
    599             String size = target.getStringAttr(ANDROID_URI, sizeAttribute);
    600             if (size != null && !size.startsWith("0")) { //$NON-NLS-1$
    601                 duplicate0dip = false;
    602                 break;
    603             }
    604         }
    605         if (duplicateWeight && sameWeight != null) {
    606             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, sameWeight);
    607             if (duplicate0dip) {
    608                 node.setAttribute(ANDROID_URI, sizeAttribute, VALUE_ZERO_DP);
    609             }
    610         }
    611     }
    612 
    613     /** A possible match position */
    614     private static class MatchPos {
    615         /** The pixel distance */
    616         private int mDistance;
    617         /** The position among siblings */
    618         private int mPosition;
    619 
    620         public MatchPos(int distance, int position) {
    621             this.mDistance = distance;
    622             this.mPosition = position;
    623         }
    624 
    625         @Override
    626         public String toString() {
    627             return "MatchPos [distance=" + mDistance //$NON-NLS-1$
    628                     + ", position=" + mPosition      //$NON-NLS-1$
    629                     + "]";                           //$NON-NLS-1$
    630         }
    631 
    632         private int getDistance() {
    633             return mDistance;
    634         }
    635 
    636         private int getPosition() {
    637             return mPosition;
    638         }
    639     }
    640 
    641     private static class LinearDropData {
    642         /** Vertical layout? */
    643         private final boolean mVertical;
    644 
    645         /** Insert points (pixels + index) */
    646         private final List<MatchPos> mIndexes;
    647 
    648         /** Number of insert positions in the target node */
    649         private final int mNumPositions;
    650 
    651         /** Current marker X position */
    652         private Integer mCurrX;
    653 
    654         /** Current marker Y position */
    655         private Integer mCurrY;
    656 
    657         /** Position of the dragged element in this layout (or
    658             -1 if the dragged element is from elsewhere) */
    659         private final int mSelfPos;
    660 
    661         /** Current drop insert index (-1 for "at the end") */
    662         private int mInsertPos = -1;
    663 
    664         /** width of match line if it's a horizontal one */
    665         private Integer mWidth;
    666 
    667         /** height of match line if it's a vertical one */
    668         private Integer mHeight;
    669 
    670         public LinearDropData(List<MatchPos> indexes, int numPositions,
    671                 boolean isVertical, int selfPos) {
    672             this.mIndexes = indexes;
    673             this.mNumPositions = numPositions;
    674             this.mVertical = isVertical;
    675             this.mSelfPos = selfPos;
    676         }
    677 
    678         @Override
    679         public String toString() {
    680             return "LinearDropData [currX=" + mCurrX //$NON-NLS-1$
    681                     + ", currY=" + mCurrY //$NON-NLS-1$
    682                     + ", height=" + mHeight //$NON-NLS-1$
    683                     + ", indexes=" + mIndexes //$NON-NLS-1$
    684                     + ", insertPos=" + mInsertPos //$NON-NLS-1$
    685                     + ", isVertical=" + mVertical //$NON-NLS-1$
    686                     + ", selfPos=" + mSelfPos //$NON-NLS-1$
    687                     + ", width=" + mWidth //$NON-NLS-1$
    688                     + "]"; //$NON-NLS-1$
    689         }
    690 
    691         private boolean isVertical() {
    692             return mVertical;
    693         }
    694 
    695         private void setCurrX(Integer currX) {
    696             this.mCurrX = currX;
    697         }
    698 
    699         private Integer getCurrX() {
    700             return mCurrX;
    701         }
    702 
    703         private void setCurrY(Integer currY) {
    704             this.mCurrY = currY;
    705         }
    706 
    707         private Integer getCurrY() {
    708             return mCurrY;
    709         }
    710 
    711         private int getSelfPos() {
    712             return mSelfPos;
    713         }
    714 
    715         private void setInsertPos(int insertPos) {
    716             this.mInsertPos = insertPos;
    717         }
    718 
    719         private int getInsertPos() {
    720             return mInsertPos;
    721         }
    722 
    723         private List<MatchPos> getIndexes() {
    724             return mIndexes;
    725         }
    726 
    727         private void setWidth(Integer width) {
    728             this.mWidth = width;
    729         }
    730 
    731         private Integer getWidth() {
    732             return mWidth;
    733         }
    734 
    735         private void setHeight(Integer height) {
    736             this.mHeight = height;
    737         }
    738 
    739         private Integer getHeight() {
    740             return mHeight;
    741         }
    742 
    743         /**
    744          * Returns true if we are inserting into the last position
    745          *
    746          * @return true if we are inserting into the last position
    747          */
    748         public boolean isLastPosition() {
    749             return mInsertPos == mNumPositions - 1;
    750         }
    751     }
    752 
    753     /** Custom resize state used during linear layout resizing */
    754     private class LinearResizeState extends ResizeState {
    755         /** Whether the node should be assigned a new weight */
    756         public boolean useWeight;
    757         /** Weight sum to be applied to the parent */
    758         private float mNewWeightSum;
    759         /** The weight to be set on the node (provided {@link #useWeight} is true) */
    760         private float mWeight;
    761         /** Map from nodes to preferred bounds of nodes where the weights have been cleared */
    762         public final Map<INode, Rect> unweightedSizes;
    763         /** Total required size required by the siblings <b>without</b> weights */
    764         public int totalLength;
    765         /** List of nodes which should have their weights cleared */
    766         public List<INode> mClearWeights;
    767 
    768         private LinearResizeState(BaseLayoutRule rule, INode layout, Object layoutView,
    769                 INode node) {
    770             super(rule, layout, layoutView, node);
    771 
    772             unweightedSizes = mRulesEngine.measureChildren(layout,
    773                     new IClientRulesEngine.AttributeFilter() {
    774                         public String getAttribute(INode n, String namespace, String localName) {
    775                             // Clear out layout weights; we need to measure the unweighted sizes
    776                             // of the children
    777                             if (ATTR_LAYOUT_WEIGHT.equals(localName)
    778                                     && SdkConstants.NS_RESOURCES.equals(namespace)) {
    779                                 return ""; //$NON-NLS-1$
    780                             }
    781 
    782                             return null;
    783                         }
    784                     });
    785 
    786             // Compute total required size required by the siblings *without* weights
    787             totalLength = 0;
    788             final boolean isVertical = isVertical(layout);
    789             for (Map.Entry<INode, Rect> entry : unweightedSizes.entrySet()) {
    790                 Rect preferredSize = entry.getValue();
    791                 if (isVertical) {
    792                     totalLength += preferredSize.h;
    793                 } else {
    794                     totalLength += preferredSize.w;
    795                 }
    796             }
    797         }
    798 
    799         /** Resets the computed state */
    800         void reset() {
    801             mNewWeightSum = -1;
    802             useWeight = false;
    803             mClearWeights = null;
    804         }
    805 
    806         /** Sets a weight to be applied to the node */
    807         void setWeight(float weight) {
    808             useWeight = true;
    809             mWeight = weight;
    810         }
    811 
    812         /** Sets a weight sum to be applied to the parent layout */
    813         void setWeightSum(float weightSum) {
    814             mNewWeightSum = weightSum;
    815         }
    816 
    817         /** Marks that the given node should be cleared when applying the new size */
    818         void clearWeight(INode n) {
    819             if (mClearWeights == null) {
    820                 mClearWeights = new ArrayList<INode>();
    821             }
    822             mClearWeights.add(n);
    823         }
    824 
    825         /** Applies the state to the nodes */
    826         public void apply() {
    827             assert useWeight;
    828 
    829             String value = mWeight > 0 ? formatFloatAttribute(mWeight) : null;
    830             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, value);
    831 
    832             if (mClearWeights != null) {
    833                 for (INode n : mClearWeights) {
    834                     if (getWeight(n) > 0.0f) {
    835                         n.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null);
    836                     }
    837                 }
    838             }
    839 
    840             if (mNewWeightSum > 0.0) {
    841                 layout.setAttribute(ANDROID_URI, ATTR_WEIGHT_SUM,
    842                         formatFloatAttribute(mNewWeightSum));
    843             }
    844         }
    845     }
    846 
    847     @Override
    848     protected ResizeState createResizeState(INode layout, Object layoutView, INode node) {
    849         return new LinearResizeState(this, layout, layoutView, node);
    850     }
    851 
    852     protected void updateResizeState(LinearResizeState resizeState, final INode node, INode layout,
    853             Rect oldBounds, Rect newBounds, SegmentType horizontalEdge,
    854             SegmentType verticalEdge) {
    855         // Update the resize state.
    856         // This method attempts to compute a new layout weight to be used in the direction
    857         // of the linear layout. If the superclass has already determined that we can snap to
    858         // a wrap_content or match_parent boundary, we prefer that. Otherwise, we attempt to
    859         // compute a layout weight - which can fail if the size is too big (not enough room),
    860         // or if the size is too small (smaller than the natural width of the node), and so on.
    861         // In that case this method just aborts, which will leave the resize state object
    862         // in such a state that it will call the superclass to resize instead, which will fall
    863         // back to device independent pixel sizing.
    864         resizeState.reset();
    865 
    866         if (oldBounds.equals(newBounds)) {
    867             return;
    868         }
    869 
    870         // If we're setting the width/height to wrap_content/match_parent in the dimension of the
    871         // linear layout, then just apply wrap_content and clear weights.
    872         boolean isVertical = isVertical(layout);
    873         if (!isVertical && verticalEdge != null) {
    874             if (resizeState.wrapWidth || resizeState.fillWidth) {
    875                 resizeState.clearWeight(node);
    876                 return;
    877             }
    878             if (newBounds.w == oldBounds.w) {
    879                 return;
    880             }
    881         }
    882 
    883         if (isVertical && horizontalEdge != null) {
    884             if (resizeState.wrapHeight || resizeState.fillHeight) {
    885                 resizeState.clearWeight(node);
    886                 return;
    887             }
    888             if (newBounds.h == oldBounds.h) {
    889                 return;
    890             }
    891         }
    892 
    893         // Compute weight sum
    894         float sum = getWeightSum(layout);
    895         if (sum <= 0.0f) {
    896             sum = 1.0f;
    897             resizeState.setWeightSum(sum);
    898         }
    899 
    900         // If the new size of the node is smaller than its preferred/wrap_content size,
    901         // then we cannot use weights to size it; switch to pixel-based sizing instead
    902         Map<INode, Rect> sizes = resizeState.unweightedSizes;
    903         Rect nodePreferredSize = sizes.get(node);
    904         if (nodePreferredSize != null) {
    905             if (horizontalEdge != null && newBounds.h < nodePreferredSize.h ||
    906                     verticalEdge != null && newBounds.w < nodePreferredSize.w) {
    907                 return;
    908             }
    909         }
    910 
    911         Rect layoutBounds = layout.getBounds();
    912         int remaining = (isVertical ? layoutBounds.h : layoutBounds.w) - resizeState.totalLength;
    913         Rect nodeBounds = sizes.get(node);
    914         if (nodeBounds == null) {
    915             return;
    916         }
    917 
    918         if (remaining > 0) {
    919             int missing = 0;
    920             if (isVertical) {
    921                 if (newBounds.h > nodeBounds.h) {
    922                     missing = newBounds.h - nodeBounds.h;
    923                 } else if (newBounds.h > resizeState.wrapBounds.h) {
    924                     // The weights concern how much space to ADD to the view.
    925                     // What if we have resized it to a size *smaller* than its current
    926                     // size without the weight delta? This can happen if you for example
    927                     // have set a hardcoded size, such as 500dp, and then size it to some
    928                     // smaller size.
    929                     missing = newBounds.h - resizeState.wrapBounds.h;
    930                     remaining += nodeBounds.h - resizeState.wrapBounds.h;
    931                     resizeState.wrapHeight = true;
    932                 }
    933             } else {
    934                 if (newBounds.w > nodeBounds.w) {
    935                     missing = newBounds.w - nodeBounds.w;
    936                 } else if (newBounds.w > resizeState.wrapBounds.w) {
    937                     missing = newBounds.w - resizeState.wrapBounds.w;
    938                     remaining += nodeBounds.w - resizeState.wrapBounds.w;
    939                     resizeState.wrapWidth = true;
    940                 }
    941             }
    942             if (missing > 0) {
    943                 // (weight / weightSum) * remaining = missing, so
    944                 // weight = missing * weightSum / remaining
    945                 float weight = missing * sum / remaining;
    946                 resizeState.setWeight(weight);
    947             }
    948         }
    949     }
    950 
    951     /**
    952      * {@inheritDoc}
    953      * <p>
    954      * Overridden in this layout in order to make resizing affect the layout_weight
    955      * attribute instead of the layout_width (for horizontal LinearLayouts) or
    956      * layout_height (for vertical LinearLayouts).
    957      */
    958     @Override
    959     protected void setNewSizeBounds(ResizeState state, final INode node, INode layout,
    960             Rect oldBounds, Rect newBounds, SegmentType horizontalEdge,
    961             SegmentType verticalEdge) {
    962         LinearResizeState resizeState = (LinearResizeState) state;
    963         updateResizeState(resizeState, node, layout, oldBounds, newBounds,
    964                 horizontalEdge, verticalEdge);
    965 
    966         if (resizeState.useWeight) {
    967             resizeState.apply();
    968 
    969             // Handle resizing in the opposite dimension of the layout
    970             final boolean isVertical = isVertical(layout);
    971             if (!isVertical && horizontalEdge != null) {
    972                 if (newBounds.h != oldBounds.h || resizeState.wrapHeight
    973                         || resizeState.fillHeight) {
    974                     node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT,
    975                             resizeState.getHeightAttribute());
    976                 }
    977             }
    978             if (isVertical && verticalEdge != null) {
    979                 if (newBounds.w != oldBounds.w || resizeState.wrapWidth || resizeState.fillWidth) {
    980                     node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH,
    981                             resizeState.getWidthAttribute());
    982                 }
    983             }
    984         } else {
    985             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, null);
    986             super.setNewSizeBounds(resizeState, node, layout, oldBounds, newBounds,
    987                     horizontalEdge, verticalEdge);
    988         }
    989     }
    990 
    991     @Override
    992     protected String getResizeUpdateMessage(ResizeState state, INode child, INode parent,
    993             Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) {
    994         LinearResizeState resizeState = (LinearResizeState) state;
    995         updateResizeState(resizeState, child, parent, child.getBounds(), newBounds,
    996                 horizontalEdge, verticalEdge);
    997 
    998         if (resizeState.useWeight) {
    999             String weight = formatFloatAttribute(resizeState.mWeight);
   1000             String dimension = String.format("weight %1$s", weight);
   1001 
   1002             String width;
   1003             String height;
   1004             if (isVertical(parent)) {
   1005                 width = resizeState.getWidthAttribute();
   1006                 height = dimension;
   1007             } else {
   1008                 width = dimension;
   1009                 height = resizeState.getHeightAttribute();
   1010             }
   1011 
   1012             if (horizontalEdge == null) {
   1013                 return width;
   1014             } else if (verticalEdge == null) {
   1015                 return height;
   1016             } else {
   1017                 // U+00D7: Unicode for multiplication sign
   1018                 return String.format("%s \u00D7 %s", width, height);
   1019             }
   1020         } else {
   1021             return super.getResizeUpdateMessage(state, child, parent, newBounds,
   1022                     horizontalEdge, verticalEdge);
   1023         }
   1024     }
   1025 
   1026     /**
   1027      * Returns the layout weight of of the given child of a LinearLayout, or 0.0 if it
   1028      * does not define a weight
   1029      */
   1030     private static float getWeight(INode linearLayoutChild) {
   1031         String weight = linearLayoutChild.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WEIGHT);
   1032         if (weight != null && weight.length() > 0) {
   1033             try {
   1034                 return Float.parseFloat(weight);
   1035             } catch (NumberFormatException nfe) {
   1036                 AdtPlugin.log(nfe, "Invalid weight %1$s", weight);
   1037             }
   1038         }
   1039 
   1040         return 0.0f;
   1041     }
   1042 
   1043     /**
   1044      * Returns the sum of all the layout weights of the children in the given LinearLayout
   1045      *
   1046      * @param linearLayout the layout to compute the total sum for
   1047      * @return the total sum of all the layout weights in the given layout
   1048      */
   1049     private static float getWeightSum(INode linearLayout) {
   1050         String weightSum = linearLayout.getStringAttr(ANDROID_URI,
   1051                 ATTR_WEIGHT_SUM);
   1052         float sum = -1.0f;
   1053         if (weightSum != null) {
   1054             // Distribute
   1055             try {
   1056                 sum = Float.parseFloat(weightSum);
   1057                 return sum;
   1058             } catch (NumberFormatException nfe) {
   1059                 // Just keep using the default
   1060             }
   1061         }
   1062 
   1063         return getSumOfWeights(linearLayout);
   1064     }
   1065 
   1066     private static float getSumOfWeights(INode linearLayout) {
   1067         float sum = 0.0f;
   1068         for (INode child : linearLayout.getChildren()) {
   1069             sum += getWeight(child);
   1070         }
   1071 
   1072         return sum;
   1073     }
   1074 
   1075     @VisibleForTesting
   1076     static String formatFloatAttribute(float value) {
   1077         if (value != (int) value) {
   1078             // Run String.format without a locale, because we don't want locale-specific
   1079             // conversions here like separating the decimal part with a comma instead of a dot!
   1080             return String.format((Locale) null, "%.2f", value); //$NON-NLS-1$
   1081         } else {
   1082             return Integer.toString((int) value);
   1083         }
   1084     }
   1085 }
   1086