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_GRAVITY;
     21 import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
     22 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ABOVE;
     23 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE;
     24 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
     25 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_LEFT;
     26 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
     27 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
     28 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
     29 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
     30 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_RIGHT;
     31 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_TOP;
     32 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING;
     33 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW;
     34 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
     35 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_IN_PARENT;
     36 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL;
     37 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX;
     38 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF;
     39 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF;
     40 import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
     41 import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
     42 import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE;
     43 
     44 import com.android.ide.common.api.DropFeedback;
     45 import com.android.ide.common.api.IDragElement;
     46 import com.android.ide.common.api.IGraphics;
     47 import com.android.ide.common.api.IMenuCallback;
     48 import com.android.ide.common.api.INode;
     49 import com.android.ide.common.api.INode.IAttribute;
     50 import com.android.ide.common.api.INodeHandler;
     51 import com.android.ide.common.api.IViewRule;
     52 import com.android.ide.common.api.InsertType;
     53 import com.android.ide.common.api.Point;
     54 import com.android.ide.common.api.Rect;
     55 import com.android.ide.common.api.RuleAction;
     56 import com.android.ide.common.api.SegmentType;
     57 import com.android.ide.common.layout.relative.ConstraintPainter;
     58 import com.android.ide.common.layout.relative.GuidelinePainter;
     59 import com.android.ide.common.layout.relative.MoveHandler;
     60 import com.android.ide.common.layout.relative.ResizeHandler;
     61 import com.android.util.Pair;
     62 
     63 import java.net.URL;
     64 import java.util.ArrayList;
     65 import java.util.Arrays;
     66 import java.util.Collections;
     67 import java.util.HashSet;
     68 import java.util.List;
     69 import java.util.Map;
     70 import java.util.Set;
     71 
     72 /**
     73  * An {@link IViewRule} for android.widget.RelativeLayout and all its derived
     74  * classes.
     75  */
     76 public class RelativeLayoutRule extends BaseLayoutRule {
     77     private static final String ACTION_SHOW_STRUCTURE = "_structure"; //$NON-NLS-1$
     78     private static final String ACTION_SHOW_CONSTRAINTS = "_constraints"; //$NON-NLS-1$
     79     private static final String ACTION_CENTER_VERTICAL = "_centerVert"; //$NON-NLS-1$
     80     private static final String ACTION_CENTER_HORIZONTAL = "_centerHoriz"; //$NON-NLS-1$
     81     private static final URL ICON_CENTER_VERTICALLY =
     82         RelativeLayoutRule.class.getResource("centerVertically.png"); //$NON-NLS-1$
     83     private static final URL ICON_CENTER_HORIZONTALLY =
     84         RelativeLayoutRule.class.getResource("centerHorizontally.png"); //$NON-NLS-1$
     85     private static final URL ICON_SHOW_STRUCTURE =
     86         BaseLayoutRule.class.getResource("structure.png"); //$NON-NLS-1$
     87     private static final URL ICON_SHOW_CONSTRAINTS =
     88         BaseLayoutRule.class.getResource("constraints.png"); //$NON-NLS-1$
     89 
     90     public static boolean sShowStructure = false;
     91     public static boolean sShowConstraints = true;
     92 
     93     // ==== Selection ====
     94 
     95     @Override
     96     public List<String> getSelectionHint(INode parentNode, INode childNode) {
     97         List<String> infos = new ArrayList<String>(18);
     98         addAttr(ATTR_LAYOUT_ABOVE, childNode, infos);
     99         addAttr(ATTR_LAYOUT_BELOW, childNode, infos);
    100         addAttr(ATTR_LAYOUT_TO_LEFT_OF, childNode, infos);
    101         addAttr(ATTR_LAYOUT_TO_RIGHT_OF, childNode, infos);
    102         addAttr(ATTR_LAYOUT_ALIGN_BASELINE, childNode, infos);
    103         addAttr(ATTR_LAYOUT_ALIGN_TOP, childNode, infos);
    104         addAttr(ATTR_LAYOUT_ALIGN_BOTTOM, childNode, infos);
    105         addAttr(ATTR_LAYOUT_ALIGN_LEFT, childNode, infos);
    106         addAttr(ATTR_LAYOUT_ALIGN_RIGHT, childNode, infos);
    107         addAttr(ATTR_LAYOUT_ALIGN_PARENT_TOP, childNode, infos);
    108         addAttr(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, childNode, infos);
    109         addAttr(ATTR_LAYOUT_ALIGN_PARENT_LEFT, childNode, infos);
    110         addAttr(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, childNode, infos);
    111         addAttr(ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING, childNode, infos);
    112         addAttr(ATTR_LAYOUT_CENTER_HORIZONTAL, childNode, infos);
    113         addAttr(ATTR_LAYOUT_CENTER_IN_PARENT, childNode, infos);
    114         addAttr(ATTR_LAYOUT_CENTER_VERTICAL, childNode, infos);
    115 
    116         return infos;
    117     }
    118 
    119     private void addAttr(String propertyName, INode childNode, List<String> infos) {
    120         String a = childNode.getStringAttr(ANDROID_URI, propertyName);
    121         if (a != null && a.length() > 0) {
    122             // Display the layout parameters without the leading layout_ prefix
    123             // and id references without the @+id/ prefix
    124             if (propertyName.startsWith(ATTR_LAYOUT_PREFIX)) {
    125                 propertyName = propertyName.substring(ATTR_LAYOUT_PREFIX.length());
    126             }
    127             a = stripIdPrefix(a);
    128             String s = propertyName + ": " + a;
    129             infos.add(s);
    130         }
    131     }
    132 
    133     @Override
    134     public void paintSelectionFeedback(IGraphics graphics, INode parentNode,
    135             List<? extends INode> childNodes, Object view) {
    136         super.paintSelectionFeedback(graphics, parentNode, childNodes, view);
    137 
    138         boolean showDependents = true;
    139         if (sShowStructure) {
    140             childNodes = Arrays.asList(parentNode.getChildren());
    141             // Avoid painting twice - both as incoming and outgoing
    142             showDependents = false;
    143         } else if (!sShowConstraints) {
    144             return;
    145         }
    146 
    147         ConstraintPainter.paintSelectionFeedback(graphics, parentNode, childNodes, showDependents);
    148     }
    149 
    150     // ==== Drag'n'drop support ====
    151 
    152     @Override
    153     public DropFeedback onDropEnter(INode targetNode, Object targetView, IDragElement[] elements) {
    154         return new DropFeedback(new MoveHandler(targetNode, elements, mRulesEngine),
    155                 new GuidelinePainter());
    156     }
    157 
    158     @Override
    159     public DropFeedback onDropMove(INode targetNode, IDragElement[] elements,
    160             DropFeedback feedback, Point p) {
    161         if (elements == null || elements.length == 0) {
    162             return null;
    163         }
    164 
    165         MoveHandler state = (MoveHandler) feedback.userData;
    166         int offsetX = p.x + (feedback.dragBounds != null ? feedback.dragBounds.x : 0);
    167         int offsetY = p.y + (feedback.dragBounds != null ? feedback.dragBounds.y : 0);
    168         state.updateMove(feedback, elements, offsetX, offsetY, feedback.modifierMask);
    169 
    170         // Or maybe only do this if the results changed...
    171         feedback.requestPaint = true;
    172 
    173         return feedback;
    174     }
    175 
    176     @Override
    177     public void onDropLeave(INode targetNode, IDragElement[] elements, DropFeedback feedback) {
    178     }
    179 
    180     @Override
    181     public void onDropped(final INode targetNode, final IDragElement[] elements,
    182             final DropFeedback feedback, final Point p) {
    183         final MoveHandler state = (MoveHandler) feedback.userData;
    184 
    185         final Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements,
    186                 feedback.isCopy || !feedback.sameCanvas);
    187 
    188         targetNode.editXml("Dropped", new INodeHandler() {
    189             @Override
    190             public void handle(INode n) {
    191                 int index = -1;
    192 
    193                 // Remove cycles
    194                 state.removeCycles();
    195 
    196                 // Now write the new elements.
    197                 INode previous = null;
    198                 for (IDragElement element : elements) {
    199                     String fqcn = element.getFqcn();
    200 
    201                     // index==-1 means to insert at the end.
    202                     // Otherwise increment the insertion position.
    203                     if (index >= 0) {
    204                         index++;
    205                     }
    206 
    207                     INode newChild = targetNode.insertChildAt(fqcn, index);
    208 
    209                     // Copy all the attributes, modifying them as needed.
    210                     addAttributes(newChild, element, idMap, BaseLayoutRule.DEFAULT_ATTR_FILTER);
    211                     addInnerElements(newChild, element, idMap);
    212 
    213                     if (previous == null) {
    214                         state.applyConstraints(newChild);
    215                         previous = newChild;
    216                     } else {
    217                         // Arrange the nodes next to each other, depending on which
    218                         // edge we are attaching to. For example, if attaching to the
    219                         // top edge, arrange the subsequent nodes in a column below it.
    220                         //
    221                         // TODO: Try to do something smarter here where we detect
    222                         // constraints between the dragged edges, and we preserve these.
    223                         // We have to do this carefully though because if the
    224                         // constraints go through some other nodes not part of the
    225                         // selection, this doesn't work right, and you might be
    226                         // dragging several connected components, which we'd then
    227                         // need to stitch together such that they are all visible.
    228 
    229                         state.attachPrevious(previous, newChild);
    230                         previous = newChild;
    231                     }
    232                 }
    233             }
    234         });
    235     }
    236 
    237     @Override
    238     public void onChildInserted(INode node, INode parent, InsertType insertType) {
    239         // TODO: Handle more generically some way to ensure that widgets with no
    240         // intrinsic size get some minimum size until they are attached on multiple
    241         // opposing sides.
    242         //String fqcn = node.getFqcn();
    243         //if (fqcn.equals(FQCN_EDIT_TEXT)) {
    244         //    node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, "100dp"); //$NON-NLS-1$
    245         //}
    246     }
    247 
    248     @Override
    249     public void onRemovingChildren(List<INode> deleted, INode parent) {
    250         super.onRemovingChildren(deleted, parent);
    251 
    252         // Remove any attachments pointing to the deleted nodes.
    253 
    254         // Produce set of attribute values that we want to delete if
    255         // present in a layout attribute
    256         Set<String> removeValues = new HashSet<String>(deleted.size() * 2);
    257         for (INode node : deleted) {
    258             String id = node.getStringAttr(ANDROID_URI, ATTR_ID);
    259             if (id != null) {
    260                 removeValues.add(id);
    261                 if (id.startsWith(NEW_ID_PREFIX)) {
    262                     removeValues.add(ID_PREFIX + stripIdPrefix(id));
    263                 } else {
    264                     removeValues.add(NEW_ID_PREFIX + stripIdPrefix(id));
    265                 }
    266             }
    267         }
    268 
    269         for (INode child : parent.getChildren()) {
    270             if (deleted.contains(child)) {
    271                 continue;
    272             }
    273             for (IAttribute attribute : child.getLiveAttributes()) {
    274                 if (attribute.getName().startsWith(ATTR_LAYOUT_PREFIX) &&
    275                         ANDROID_URI.equals(attribute.getUri())) {
    276                     String value = attribute.getValue();
    277                     if (removeValues.contains(value)) {
    278                         // Unset this reference to a deleted widget.
    279                         child.setAttribute(ANDROID_URI, attribute.getName(), null);
    280                     }
    281                 }
    282             }
    283         }
    284     }
    285 
    286     // ==== Resize Support ====
    287 
    288     @Override
    289     public DropFeedback onResizeBegin(INode child, INode parent,
    290             SegmentType horizontalEdgeType, SegmentType verticalEdgeType,
    291             Object childView, Object parentView) {
    292         ResizeHandler state = new ResizeHandler(parent, child, mRulesEngine,
    293                 horizontalEdgeType, verticalEdgeType);
    294         return new DropFeedback(state, new GuidelinePainter());
    295     }
    296 
    297     @Override
    298     public void onResizeUpdate(DropFeedback feedback, INode child, INode parent, Rect newBounds,
    299             int modifierMask) {
    300         ResizeHandler state = (ResizeHandler) feedback.userData;
    301         state.updateResize(feedback, child, newBounds, modifierMask);
    302     }
    303 
    304     @Override
    305     public void onResizeEnd(DropFeedback feedback, INode child, INode parent,
    306             final Rect newBounds) {
    307         final ResizeHandler state = (ResizeHandler) feedback.userData;
    308 
    309         child.editXml("Resize", new INodeHandler() {
    310             @Override
    311             public void handle(INode n) {
    312                 state.removeCycles();
    313                 state.applyConstraints(n);
    314             }
    315         });
    316     }
    317 
    318     // ==== Layout Actions Bar ====
    319 
    320     @Override
    321     public void addLayoutActions(List<RuleAction> actions, final INode parentNode,
    322             final List<? extends INode> children) {
    323         super.addLayoutActions(actions, parentNode, children);
    324 
    325         actions.add(createGravityAction(Collections.<INode>singletonList(parentNode),
    326                 ATTR_GRAVITY));
    327         actions.add(RuleAction.createSeparator(25));
    328         actions.add(createMarginAction(parentNode, children));
    329 
    330         IMenuCallback callback = new IMenuCallback() {
    331             @Override
    332             public void action(RuleAction action, List<? extends INode> selectedNodes,
    333                     final String valueId, final Boolean newValue) {
    334                 final String id = action.getId();
    335                 if (id.equals(ACTION_CENTER_VERTICAL)|| id.equals(ACTION_CENTER_HORIZONTAL)) {
    336                     parentNode.editXml("Center", new INodeHandler() {
    337                         @Override
    338                         public void handle(INode n) {
    339                             if (id.equals(ACTION_CENTER_VERTICAL)) {
    340                                 for (INode child : children) {
    341                                     centerVertically(child);
    342                                 }
    343                             } else if (id.equals(ACTION_CENTER_HORIZONTAL)) {
    344                                 for (INode child : children) {
    345                                     centerHorizontally(child);
    346                                 }
    347                             }
    348                             mRulesEngine.redraw();
    349                         }
    350 
    351                     });
    352                 } else if (id.equals(ACTION_SHOW_CONSTRAINTS)) {
    353                     sShowConstraints = !sShowConstraints;
    354                     mRulesEngine.redraw();
    355                 } else {
    356                     assert id.equals(ACTION_SHOW_STRUCTURE);
    357                     sShowStructure = !sShowStructure;
    358                     mRulesEngine.redraw();
    359                 }
    360             }
    361         };
    362 
    363         // Centering actions
    364         if (children != null && children.size() > 0) {
    365                         actions.add(RuleAction.createSeparator(150));
    366             actions.add(RuleAction.createAction(ACTION_CENTER_VERTICAL, "Center Vertically",
    367                     callback, ICON_CENTER_VERTICALLY, 160, false));
    368             actions.add(RuleAction.createAction(ACTION_CENTER_HORIZONTAL, "Center Horizontally",
    369                     callback, ICON_CENTER_HORIZONTALLY, 170, false));
    370         }
    371 
    372         actions.add(RuleAction.createSeparator(80));
    373         actions.add(RuleAction.createToggle(ACTION_SHOW_CONSTRAINTS, "Show Constraints",
    374                 sShowConstraints, callback, ICON_SHOW_CONSTRAINTS, 180, false));
    375         actions.add(RuleAction.createToggle(ACTION_SHOW_STRUCTURE, "Show All Relationships",
    376                 sShowStructure, callback, ICON_SHOW_STRUCTURE, 190, false));
    377     }
    378 
    379     private void centerHorizontally(INode node) {
    380         // Clear horizontal-oriented attributes from the node
    381         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_LEFT, null);
    382         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_LEFT, null);
    383         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF, null);
    384         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
    385         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_RIGHT, null);
    386         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_RIGHT, null);
    387         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF, null);
    388         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
    389 
    390         if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT))) {
    391             // Already done
    392         } else if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI,
    393                 ATTR_LAYOUT_CENTER_VERTICAL))) {
    394             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
    395             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, VALUE_TRUE);
    396         } else {
    397             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, VALUE_TRUE);
    398         }
    399     }
    400 
    401     private void centerVertically(INode node) {
    402         // Clear vertical-oriented attributes from the node
    403         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_TOP, null);
    404         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_TOP, null);
    405         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_BELOW, null);
    406         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, null);
    407         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BOTTOM, null);
    408         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ABOVE, null);
    409         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null);
    410         node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null);
    411 
    412         // Center vertically
    413         if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT))) {
    414             // ALready done
    415         } else if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI,
    416                 ATTR_LAYOUT_CENTER_HORIZONTAL))) {
    417             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null);
    418             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, VALUE_TRUE);
    419         } else {
    420             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, VALUE_TRUE);
    421         }
    422     }
    423 }
    424