• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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