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 getSelectionHint(INode parentNode, INode childNode)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 addAttr(String propertyName, INode childNode, List<String> infos)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 paintSelectionFeedback(IGraphics graphics, INode parentNode, List<? extends INode> childNodes, Object view)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 onDropEnter(INode targetNode, Object targetView, IDragElement[] elements)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 onDropMove(INode targetNode, IDragElement[] elements, DropFeedback feedback, Point p)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 onDropLeave(INode targetNode, IDragElement[] elements, DropFeedback feedback)177 public void onDropLeave(INode targetNode, IDragElement[] elements, DropFeedback feedback) { 178 } 179 180 @Override onDropped(final INode targetNode, final IDragElement[] elements, final DropFeedback feedback, final Point p)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 public void handle(INode n) { 190 int index = -1; 191 192 // Remove cycles 193 state.removeCycles(); 194 195 // Now write the new elements. 196 INode previous = null; 197 for (IDragElement element : elements) { 198 String fqcn = element.getFqcn(); 199 200 // index==-1 means to insert at the end. 201 // Otherwise increment the insertion position. 202 if (index >= 0) { 203 index++; 204 } 205 206 INode newChild = targetNode.insertChildAt(fqcn, index); 207 208 // Copy all the attributes, modifying them as needed. 209 addAttributes(newChild, element, idMap, BaseLayoutRule.DEFAULT_ATTR_FILTER); 210 addInnerElements(newChild, element, idMap); 211 212 if (previous == null) { 213 state.applyConstraints(newChild); 214 previous = newChild; 215 } else { 216 // Arrange the nodes next to each other, depending on which 217 // edge we are attaching to. For example, if attaching to the 218 // top edge, arrange the subsequent nodes in a column below it. 219 // 220 // TODO: Try to do something smarter here where we detect 221 // constraints between the dragged edges, and we preserve these. 222 // We have to do this carefully though because if the 223 // constraints go through some other nodes not part of the 224 // selection, this doesn't work right, and you might be 225 // dragging several connected components, which we'd then 226 // need to stitch together such that they are all visible. 227 228 state.attachPrevious(previous, newChild); 229 previous = newChild; 230 } 231 } 232 } 233 }); 234 } 235 236 @Override onChildInserted(INode node, INode parent, InsertType insertType)237 public void onChildInserted(INode node, INode parent, InsertType insertType) { 238 // TODO: Handle more generically some way to ensure that widgets with no 239 // intrinsic size get some minimum size until they are attached on multiple 240 // opposing sides. 241 //String fqcn = node.getFqcn(); 242 //if (fqcn.equals(FQCN_EDIT_TEXT)) { 243 // node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, "100dp"); //$NON-NLS-1$ 244 //} 245 } 246 247 @Override onRemovingChildren(List<INode> deleted, INode parent)248 public void onRemovingChildren(List<INode> deleted, INode parent) { 249 super.onRemovingChildren(deleted, parent); 250 251 // Remove any attachments pointing to the deleted nodes. 252 253 // Produce set of attribute values that we want to delete if 254 // present in a layout attribute 255 Set<String> removeValues = new HashSet<String>(deleted.size() * 2); 256 for (INode node : deleted) { 257 String id = node.getStringAttr(ANDROID_URI, ATTR_ID); 258 if (id != null) { 259 removeValues.add(id); 260 if (id.startsWith(NEW_ID_PREFIX)) { 261 removeValues.add(ID_PREFIX + stripIdPrefix(id)); 262 } else { 263 removeValues.add(NEW_ID_PREFIX + stripIdPrefix(id)); 264 } 265 } 266 } 267 268 for (INode child : parent.getChildren()) { 269 if (deleted.contains(child)) { 270 continue; 271 } 272 for (IAttribute attribute : child.getLiveAttributes()) { 273 if (attribute.getName().startsWith(ATTR_LAYOUT_PREFIX) && 274 ANDROID_URI.equals(attribute.getUri())) { 275 String value = attribute.getValue(); 276 if (removeValues.contains(value)) { 277 // Unset this reference to a deleted widget. 278 child.setAttribute(ANDROID_URI, attribute.getName(), null); 279 } 280 } 281 } 282 } 283 } 284 285 // ==== Resize Support ==== 286 287 @Override onResizeBegin(INode child, INode parent, SegmentType horizontalEdgeType, SegmentType verticalEdgeType, Object childView, Object parentView)288 public DropFeedback onResizeBegin(INode child, INode parent, 289 SegmentType horizontalEdgeType, SegmentType verticalEdgeType, 290 Object childView, Object parentView) { 291 ResizeHandler state = new ResizeHandler(parent, child, mRulesEngine, 292 horizontalEdgeType, verticalEdgeType); 293 return new DropFeedback(state, new GuidelinePainter()); 294 } 295 296 @Override onResizeUpdate(DropFeedback feedback, INode child, INode parent, Rect newBounds, int modifierMask)297 public void onResizeUpdate(DropFeedback feedback, INode child, INode parent, Rect newBounds, 298 int modifierMask) { 299 ResizeHandler state = (ResizeHandler) feedback.userData; 300 state.updateResize(feedback, child, newBounds, modifierMask); 301 } 302 303 @Override onResizeEnd(DropFeedback feedback, INode child, INode parent, final Rect newBounds)304 public void onResizeEnd(DropFeedback feedback, INode child, INode parent, 305 final Rect newBounds) { 306 final ResizeHandler state = (ResizeHandler) feedback.userData; 307 308 child.editXml("Resize", new INodeHandler() { 309 public void handle(INode n) { 310 state.removeCycles(); 311 state.applyConstraints(n); 312 } 313 }); 314 } 315 316 // ==== Layout Actions Bar ==== 317 318 @Override addLayoutActions(List<RuleAction> actions, final INode parentNode, final List<? extends INode> children)319 public void addLayoutActions(List<RuleAction> actions, final INode parentNode, 320 final List<? extends INode> children) { 321 super.addLayoutActions(actions, parentNode, children); 322 323 actions.add(createGravityAction(Collections.<INode>singletonList(parentNode), 324 ATTR_GRAVITY)); 325 actions.add(RuleAction.createSeparator(25)); 326 actions.add(createMarginAction(parentNode, children)); 327 328 IMenuCallback callback = new IMenuCallback() { 329 public void action(RuleAction action, List<? extends INode> selectedNodes, 330 final String valueId, final Boolean newValue) { 331 final String id = action.getId(); 332 if (id.equals(ACTION_CENTER_VERTICAL)|| id.equals(ACTION_CENTER_HORIZONTAL)) { 333 parentNode.editXml("Center", new INodeHandler() { 334 public void handle(INode n) { 335 if (id.equals(ACTION_CENTER_VERTICAL)) { 336 for (INode child : children) { 337 centerVertically(child); 338 } 339 } else if (id.equals(ACTION_CENTER_HORIZONTAL)) { 340 for (INode child : children) { 341 centerHorizontally(child); 342 } 343 } 344 mRulesEngine.redraw(); 345 } 346 347 }); 348 } else if (id.equals(ACTION_SHOW_CONSTRAINTS)) { 349 sShowConstraints = !sShowConstraints; 350 mRulesEngine.redraw(); 351 } else { 352 assert id.equals(ACTION_SHOW_STRUCTURE); 353 sShowStructure = !sShowStructure; 354 mRulesEngine.redraw(); 355 } 356 } 357 }; 358 359 // Centering actions 360 if (children != null && children.size() > 0) { 361 actions.add(RuleAction.createSeparator(150)); 362 actions.add(RuleAction.createAction(ACTION_CENTER_VERTICAL, "Center Vertically", 363 callback, ICON_CENTER_VERTICALLY, 160, false)); 364 actions.add(RuleAction.createAction(ACTION_CENTER_HORIZONTAL, "Center Horizontally", 365 callback, ICON_CENTER_HORIZONTALLY, 170, false)); 366 } 367 368 actions.add(RuleAction.createSeparator(80)); 369 actions.add(RuleAction.createToggle(ACTION_SHOW_CONSTRAINTS, "Show Constraints", 370 sShowConstraints, callback, ICON_SHOW_CONSTRAINTS, 180, false)); 371 actions.add(RuleAction.createToggle(ACTION_SHOW_STRUCTURE, "Show All Relationships", 372 sShowStructure, callback, ICON_SHOW_STRUCTURE, 190, false)); 373 } 374 centerHorizontally(INode node)375 private void centerHorizontally(INode node) { 376 // Clear horizontal-oriented attributes from the node 377 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_LEFT, null); 378 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_LEFT, null); 379 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF, null); 380 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null); 381 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_RIGHT, null); 382 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_RIGHT, null); 383 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF, null); 384 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null); 385 386 if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT))) { 387 // Already done 388 } else if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, 389 ATTR_LAYOUT_CENTER_VERTICAL))) { 390 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null); 391 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, VALUE_TRUE); 392 } else { 393 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, VALUE_TRUE); 394 } 395 } 396 centerVertically(INode node)397 private void centerVertically(INode node) { 398 // Clear vertical-oriented attributes from the node 399 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_TOP, null); 400 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_TOP, null); 401 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_BELOW, null); 402 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, null); 403 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BOTTOM, null); 404 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ABOVE, null); 405 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, null); 406 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_ALIGN_BASELINE, null); 407 408 // Center vertically 409 if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT))) { 410 // ALready done 411 } else if (VALUE_TRUE.equals(node.getStringAttr(ANDROID_URI, 412 ATTR_LAYOUT_CENTER_HORIZONTAL))) { 413 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_HORIZONTAL, null); 414 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_IN_PARENT, VALUE_TRUE); 415 } else { 416 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_CENTER_VERTICAL, VALUE_TRUE); 417 } 418 } 419 } 420