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 getSelectionHint(@onNull INode parentNode, @NonNull INode childNode)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 addAttr(String propertyName, INode childNode, List<String> infos)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 paintSelectionFeedback(@onNull IGraphics graphics, @NonNull INode parentNode, @NonNull List<? extends INode> childNodes, @Nullable Object view)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 onDropEnter(@onNull INode targetNode, @Nullable Object targetView, @Nullable IDragElement[] elements)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 onDropMove(@onNull INode targetNode, @NonNull IDragElement[] elements, @Nullable DropFeedback feedback, @NonNull Point p)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 onDropLeave(@onNull INode targetNode, @NonNull IDragElement[] elements, @Nullable DropFeedback feedback)175 public void onDropLeave(@NonNull INode targetNode, @NonNull IDragElement[] elements, 176 @Nullable DropFeedback feedback) { 177 } 178 179 @Override onDropped(final @NonNull INode targetNode, final @NonNull IDragElement[] elements, final @Nullable DropFeedback feedback, final @NonNull Point p)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 onChildInserted(@onNull INode node, @NonNull INode parent, @NonNull InsertType insertType)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 onRemovingChildren(@onNull List<INode> deleted, @NonNull INode parent, boolean moved)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 onResizeBegin(@onNull INode child, @NonNull INode parent, @Nullable SegmentType horizontalEdgeType, @Nullable SegmentType verticalEdgeType, @Nullable Object childView, @Nullable Object parentView)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 onResizeUpdate(@ullable DropFeedback feedback, @NonNull INode child, @NonNull INode parent, @NonNull Rect newBounds, int modifierMask)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 onResizeEnd(@ullable DropFeedback feedback, @NonNull INode child, @NonNull INode parent, final @NonNull Rect newBounds)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 addLayoutActions( @onNull List<RuleAction> actions, final @NonNull INode parentNode, final @NonNull List<? extends INode> children)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 centerHorizontally(INode node)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 centerVertically(INode node)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