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