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 17package com.android.adt.gscripts; 18 19/** 20 * An {@link IViewRule} for android.widget.RelativeLayout and all its derived classes. 21 */ 22public class AndroidWidgetRelativeLayoutRule extends BaseLayout { 23 24 25 // ==== Selection ==== 26 27 /** 28 * Display some relation layout information on a selected child. 29 */ 30 void onChildSelected(IGraphics gc, INode parentNode, INode childNode) { 31 super.onChildSelected(gc, parentNode, childNode); 32 33 // Get the top parent, to display data under it 34 INode topParent = parentNode; 35 while (true) { 36 INode p = topParent.getParent(); 37 if (p == null) { 38 break; 39 } else { 40 topParent = p; 41 } 42 } 43 44 Rect b = topParent.getBounds(); 45 if (!b.isValid()) { 46 return; 47 } 48 49 def infos = []; 50 51 def addAttr = { 52 def a = childNode.getStringAttr(ANDROID_URI, "layout_${it}"); 53 if (a) { 54 infos += "${it}: ${a}"; 55 } 56 } 57 58 addAttr("above"); 59 addAttr("below"); 60 addAttr("toLeftOf"); 61 addAttr("toRightOf"); 62 addAttr("alignBaseline"); 63 addAttr("alignTop"); 64 addAttr("alignBottom"); 65 addAttr("alignLeft"); 66 addAttr("alignRight"); 67 addAttr("alignParentTop"); 68 addAttr("alignParentBottom"); 69 addAttr("alignParentLeft"); 70 addAttr("alignParentRight"); 71 addAttr("alignWithParentMissing"); 72 addAttr("centerHorizontal"); 73 addAttr("centerInParent"); 74 addAttr("centerVertical"); 75 76 if (infos) { 77 gc.setForeground(gc.registerColor(0x00222222)); 78 int x = b.x + 10; 79 int y = b.y + b.h + 10; 80 int h = gc.getFontHeight(); 81 infos.each { 82 y += h; 83 gc.drawString(it, x, y); 84 } 85 } 86 } 87 88 89 // ==== Drag'n'drop support ==== 90 91 DropFeedback onDropEnter(INode targetNode, IDragElement[] elements) { 92 93 if (elements.length == 0) { 94 return null; 95 } 96 97 def bn = targetNode.getBounds(); 98 if (!bn.isValid()) { 99 return; 100 } 101 102 // Collect the ids of the elements being dragged 103 def movedIds = collectIds([:], elements).keySet().asList(); 104 105 // Prepare the drop feedback 106 return new DropFeedback( 107 [ "child": null, // INode: Current child under cursor 108 "index": 0, // int: Index of child in the parent children list 109 "zones": null, // Valid "anchor" zones for the current child 110 // of type list(map(rect:Rect, attr:[String])) 111 "curr": null, // map: Current zone 112 "movedIds": movedIds, 113 "cachedLinkIds": [:] 114 ], 115 { gc, node, feedback -> 116 // Paint closure for the RelativeLayout just defers to the method below 117 drawRelativeDropFeedback(gc, node, elements, feedback); 118 }); 119 } 120 121 DropFeedback onDropMove(INode targetNode, 122 IDragElement[] elements, 123 DropFeedback feedback, 124 Point p) { 125 126 def data = feedback.userData; 127 Rect area = feedback.captureArea; 128 129 // Only look for a new child if cursor is no longer under the current rect 130 if (area == null || !area.contains(p)) { 131 132 // We're not capturing anymore since we got outside of the capture bounds 133 feedback.captureArea = null; 134 135 // Find the current direct children under the cursor 136 def childNode = null; 137 def childIndex = -1; 138 nextChild: for(child in targetNode.getChildren()) { 139 childIndex++; 140 def bc = child.getBounds(); 141 if (bc.contains(p)) { 142 143 // TODO visually indicate this target node has been rejected, 144 // e.g. by drawing a semi-transp rect on it or drawing a red cross at 145 // the cursor point. 146 147 // If we're doing a move operation within the same canvas, we can't 148 // attach the moved object to one belonging to the selection since 149 // it will disappear after the move. 150 if (feedback.sameCanvas && !feedback.isCopy) { 151 for (element in elements) { 152 if (bc == element.getBounds()) { 153 continue nextChild; 154 } 155 } 156 } 157 158 // One more limitation: if we're moving one or more elements, we can't 159 // drop them on a child which relative position is expressed directly or 160 // indirectly based on the element being moved. 161 if (!feedback.isCopy) { 162 if (searchRelativeIds(child, data.movedIds, data.cachedLinkIds)) { 163 continue nextChild; 164 } 165 } 166 167 childNode = child; 168 break; 169 } 170 } 171 172 // If there is a selected child and it changed, recompute child drop zones 173 if (childNode != null && childNode != data.child) { 174 data.child = childNode; 175 data.index = childIndex; 176 data.curr = null; 177 data.zones = null; 178 179 def result = computeChildDropZones(childNode); 180 data.zones = result[1]; 181 182 // capture this rect, to prevent the engine from switching the layout node. 183 feedback.captureArea = result[0]; 184 feedback.requestPaint = true; 185 186 } else if (childNode == null) { 187 // If there is no selected child, compute the border drop zone 188 data.child = null; 189 data.index = -1; 190 data.curr = null; 191 192 def zone = computeBorderDropZone(targetNode.getBounds(), p); 193 194 if (zone == null) { 195 data.zones = null; 196 } else { 197 data.zones = [ zone ]; 198 feedback.captureArea = zone.rect; 199 } 200 201 feedback.requestPaint = (area != feedback.captureArea); 202 } 203 } 204 205 // Find the current zone 206 def currZone = null; 207 if (data.zones) { 208 for(zone in data.zones) { 209 if (zone.rect.contains(p)) { 210 currZone = zone; 211 break; 212 } 213 } 214 } 215 216 if (currZone != data.curr) { 217 data.curr = currZone; 218 feedback.requestPaint = true; 219 } 220 221 return feedback; 222 } 223 224 /** 225 * Returns true if the child has any attribute of Format.REFERENCE which 226 * value matches one of the ids in movedIds. 227 */ 228 def searchRelativeIds(INode node, List movedIds, Map cachedLinkIds) { 229 def ids = getLinkedIds(node, cachedLinkIds); 230 231 for (id in ids) { 232 if (id in movedIds) { 233 return true; 234 } 235 } 236 237 return false; 238 } 239 240 def getLinkedIds(INode node, Map cachedLinkIds) { 241 def ids = cachedLinkIds[node]; 242 243 if (ids != null) { 244 return ids; 245 } 246 247 // we don't have cached data on this child, so create a list of 248 // all the linked id it is referencing. 249 ids = []; 250 cachedLinkIds[node] = ids; 251 for (attr in node.getAttributes()) { 252 def attrInfo = node.getAttributeInfo(attr.getUri(), attr.getName()); 253 if (attrInfo == null) { 254 continue; 255 } 256 def formats = attrInfo.getFormats(); 257 if (formats == null || !(IAttributeInfo.Format.REFERENCE in formats)) { 258 continue; 259 } 260 def id = attr.getValue(); 261 id = normalizeId(id); 262 if (id in ids) { 263 continue; 264 } 265 ids.add(id); 266 267 // Find the sibling with that id 268 def p = node.getParent(); 269 if (p == null) { 270 continue; 271 } 272 for (child in p.getChildren()) { 273 if (child == node) { 274 continue; 275 } 276 def childId = child.getStringAttr(ANDROID_URI, ATTR_ID); 277 childId = normalizeId(childId); 278 if (id == childId) { 279 def linkedIds = getLinkedIds(child, cachedLinkIds); 280 ids.addAll(linkedIds); 281 break; 282 } 283 } 284 } 285 286 return ids; 287 } 288 289 def computeBorderDropZone(Rect bounds, Point p) { 290 291 int x = p.x; 292 int y = p.y; 293 294 int x1 = bounds.x; 295 int y1 = bounds.y; 296 int w = bounds.w; 297 int h = bounds.h; 298 int x2 = x1 + w; 299 int y2 = y1 + h; 300 301 int n = 10; 302 int n2 = n*2; 303 304 Rect r = null; 305 String attr = null; 306 307 if (x <= x1 + n && y >= y1 && y <= y2) { 308 r = new Rect(x1 - n, y1, n2, h); 309 attr = "alignParentLeft"; 310 311 } else if (x >= x2 - n && y >= y1 && y <= y2) { 312 r = new Rect(x2 - n, y1, n2, h); 313 attr = "alignParentRight"; 314 315 } else if (y <= y1 + n && x >= x1 && x <= x2) { 316 r = new Rect(x1, y1 - n, w, n2); 317 attr = "alignParentTop"; 318 319 } else if (y >= y2 - n && x >= x1 && x <= x2) { 320 r = new Rect(x1, y2 - n, w, n2); 321 attr = "alignParentBottom"; 322 323 } else { 324 // we're nowhere near a border 325 return null; 326 } 327 328 return [ "rect": r, "attr": [ attr ], "mark": r.center() ]; 329 } 330 331 332 def computeChildDropZones(INode childNode) { 333 334 Rect b = childNode.getBounds(); 335 336 // Compute drop zone borders as follow: 337 // 338 // +---+-----+-----+-----+---+ 339 // | 1 \ 2 \ 3 / 4 / 5 | 340 // +----+-----+---+-----+----+ 341 // 342 // For the top and bottom borders, zones 1 and 5 have the same width, which is 343 // size1 = min(10, w/5) 344 // and zones 2, 3 and 4 have a width of 345 // size2 = (w - 2*size) / 3 346 // 347 // Same works for left and right borders vertically. 348 // 349 // Attributes generated: 350 // Horizontally: 351 // 1- toLeftOf / 2- alignLeft / 3- 2+4 / 4- alignRight / 5- toRightOf 352 // Vertically: 353 // 1- above / 2-alignTop / 3- 2+4 / 4- alignBottom / 5- below 354 355 int w1 = 20; 356 int w3 = b.w / 3; 357 int w2 = Math.max(20, w3); 358 359 int h1 = 20; 360 int h3 = b.h / 3; 361 int h2 = Math.max(20, h3); 362 363 int wt = w1 * 2 + w2 * 3; 364 int ht = h1 * 2 + h2 * 3; 365 366 int x1 = b.x + ((b.w - wt) / 2); 367 int y1 = b.y + ((b.h - ht) / 2); 368 369 def bounds = new Rect(x1, y1, wt, ht); 370 371 def zones = []; 372 def a = "above"; 373 int x = x1; 374 int y = y1; 375 376 def addx = { 377 int wn, ArrayList a2 -> 378 379 zones << [ "rect": new Rect(x, y, wn, h1), 380 "attr": [ a ] + a2 ]; 381 x += wn; 382 } 383 384 addx(w1, [ "toLeftOf" ]); 385 addx(w2, [ "alignLeft" ]); 386 addx(w2, [ "alignLeft", "alignRight" ]); 387 addx(w2, [ "alignRight" ]); 388 addx(w1, [ "toRightOf" ]); 389 390 a = "below"; 391 x = x1; 392 y = y1 + ht - h1; 393 394 addx(w1, [ "toLeftOf" ]); 395 addx(w2, [ "alignLeft" ]); 396 addx(w2, [ "alignLeft", "alignRight" ]); 397 addx(w2, [ "alignRight" ]); 398 addx(w1, [ "toRightOf" ]); 399 400 def addy = { 401 int hn, ArrayList a2 -> 402 zones << [ "rect": new Rect(x, y, w1, hn), 403 "attr": [ a ] + a2 ]; 404 y += hn; 405 } 406 407 a = "toLeftOf"; 408 x = x1; 409 y = y1 + h1; 410 411 addy(h2, [ "alignTop" ]); 412 addy(h2, [ "alignTop", "alignBottom" ]); 413 addy(h2, [ "alignBottom" ]); 414 415 a = "toRightOf"; 416 x = x1 + wt - w1; 417 y = y1 + h1; 418 419 addy(h2, [ "alignTop" ]); 420 addy(h2, [ "alignTop", "alignBottom" ]); 421 addy(h2, [ "alignBottom" ]); 422 423 return [ bounds, zones ]; 424 } 425 426 void drawRelativeDropFeedback(IGraphics gc, 427 INode targetNode, 428 IDragElement[] elements, 429 DropFeedback feedback) { 430 Rect b = targetNode.getBounds(); 431 if (!b.isValid()) { 432 return; 433 } 434 435 def color = gc.registerColor(0x00FF9900); 436 gc.setForeground(color); 437 438 gc.setLineStyle(IGraphics.LineStyle.LINE_SOLID); 439 gc.setLineWidth(2); 440 gc.drawRect(b); 441 442 gc.setLineStyle(IGraphics.LineStyle.LINE_DOT); 443 gc.setLineWidth(1); 444 445 def data = feedback.userData; 446 447 if (data.zones) { 448 data.zones.each { 449 gc.drawRect(it.rect); 450 } 451 } 452 453 if (data.curr) { 454 gc.setAlpha(200); 455 gc.setBackground(color); 456 gc.fillRect(data.curr.rect); 457 gc.setAlpha(255); 458 459 def r = feedback.captureArea; 460 int x = r.x + 5; 461 int y = r.y + r.h + 5; 462 int h = gc.getFontHeight(); 463 464 String id = null; 465 if (data.child) { 466 id = data.child.getStringAttr(ANDROID_URI, ATTR_ID); 467 } 468 469 for (s in data.curr.attr) { 470 if (id) s = "$s=$id"; 471 gc.drawString(s, x, y); 472 y += h; 473 } 474 475 gc.setLineStyle(IGraphics.LineStyle.LINE_SOLID); 476 gc.setLineWidth(2); 477 478 def mark = data.curr.get("mark"); 479 if (mark) { 480 def black = gc.registerColor(0); 481 gc.setForeground(black); 482 483 x = mark.x; 484 y = mark.y; 485 gc.drawLine(x - 10, y - 10, x + 10, y + 10); 486 gc.drawLine(x + 10, y - 10, x - 10, y + 10); 487 gc.drawOval(x - 10, y - 10, x + 10, y + 10); 488 489 } else { 490 491 r = data.curr.rect; 492 x = r.x + r.w / 2; 493 y = r.y + r.h / 2; 494 } 495 496 Rect be = elements[0].getBounds(); 497 498 if (be.isValid()) { 499 // At least the first element has a bound. Draw rectangles 500 // for all dropped elements with valid bounds, offset at 501 // the drop point. 502 503 int offsetX = x - be.x; 504 int offsetY = y - be.y; 505 506 if ("alignTop" in data.curr.attr && "alignBottom" in data.curr.attr) { 507 offsetY -= be.h / 2; 508 } else if ("above" in data.curr.attr || "alignTop" in data.curr.attr) { 509 offsetY -= be.h; 510 } 511 if ("alignRight" in data.curr.attr && "alignLeft" in data.curr.attr) { 512 offsetX -= be.w / 2; 513 } else if ("toLeftOf" in data.curr.attr || "alignLeft" in data.curr.attr) { 514 offsetX -= be.w; 515 } 516 517 gc.setForeground(gc.registerColor(0x00FFFF00)); 518 519 for (element in elements) { 520 drawElement(gc, element, offsetX, offsetY); 521 } 522 } 523 } 524 } 525 526 void onDropLeave(INode targetNode, IDragElement[] elements, DropFeedback feedback) { 527 // Free the last captured rect, if any 528 feedback.captureArea = null; 529 } 530 531 void onDropped(INode targetNode, 532 IDragElement[] elements, 533 DropFeedback feedback, 534 Point p) { 535 def data = feedback.userData; 536 if (!data.curr) { 537 return; 538 } 539 540 def index = data.index; 541 def insertPos = data.insertPos; 542 543 // Collect IDs from dropped elements and remap them to new IDs 544 // if this is a copy or from a different canvas. 545 def idMap = getDropIdMap(targetNode, elements, feedback.isCopy || !feedback.sameCanvas); 546 547 targetNode.editXml("Add elements to RelativeLayout") { 548 549 // Now write the new elements. 550 for (element in elements) { 551 String fqcn = element.getFqcn(); 552 Rect be = element.getBounds(); 553 554 // index==-1 means to insert at the end. 555 // Otherwise increment the insertion position. 556 if (index >= 0) { 557 index++; 558 } 559 560 INode newChild = targetNode.insertChildAt(fqcn, index); 561 562 // Copy all the attributes, modifying them as needed. 563 def attrFilter = getLayoutAttrFilter(); 564 addAttributes(newChild, element, idMap) { 565 uri, name, value -> 566 // TODO need a better way to exclude other layout attributes dynamically 567 if (uri == ANDROID_URI && name in attrFilter) { 568 return false; // don't set these attributes 569 } else { 570 return value; 571 } 572 }; 573 574// TODO... seems totally wrong. REVISIT or EXPLAIN 575 String id = null; 576 if (data.child) { 577 id = data.child.getStringAttr(ANDROID_URI, ATTR_ID); 578 } 579 580 data.curr.attr.each { 581 newChild.setAttribute(ANDROID_URI, "layout_${it}", id ? id : "true"); 582 } 583 584 addInnerElements(newChild, element, idMap); 585 } 586 } 587 } 588 589 590} 591