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