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