1 /*
2  * Copyright (C) 2011 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 package com.android.ide.common.layout.grid;
17 
18 import static com.android.SdkConstants.ATTR_COLUMN_COUNT;
19 import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
20 import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
21 import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
22 import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
23 import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
24 import static com.android.ide.common.layout.GravityHelper.getGravity;
25 import static com.android.ide.common.layout.GridLayoutRule.GRID_SIZE;
26 import static com.android.ide.common.layout.GridLayoutRule.MARGIN_SIZE;
27 import static com.android.ide.common.layout.GridLayoutRule.MAX_CELL_DIFFERENCE;
28 import static com.android.ide.common.layout.GridLayoutRule.SHORT_GAP_DP;
29 import static com.android.ide.common.layout.grid.GridModel.UNDEFINED;
30 import static java.lang.Math.abs;
31 
32 import com.android.ide.common.api.DropFeedback;
33 import com.android.ide.common.api.IDragElement;
34 import com.android.ide.common.api.INode;
35 import com.android.ide.common.api.IViewMetadata;
36 import com.android.ide.common.api.Margins;
37 import com.android.ide.common.api.Point;
38 import com.android.ide.common.api.Rect;
39 import com.android.ide.common.api.SegmentType;
40 import com.android.ide.common.layout.BaseLayoutRule;
41 import com.android.ide.common.layout.GravityHelper;
42 import com.android.ide.common.layout.GridLayoutRule;
43 import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
44 
45 import java.util.ArrayList;
46 import java.util.Collection;
47 import java.util.Collections;
48 import java.util.List;
49 import java.util.Locale;
50 
51 /**
52  * The {@link GridDropHandler} handles drag and drop operations into and within a
53  * GridLayout, computing guidelines, handling drops to edit the grid model, and so on.
54  */
55 public class GridDropHandler {
56     private final GridModel mGrid;
57     private final GridLayoutRule mRule;
58     private GridMatch mColumnMatch;
59     private GridMatch mRowMatch;
60 
61     /**
62      * Creates a new {@link GridDropHandler} for
63      * @param gridLayoutRule the corresponding {@link GridLayoutRule}
64      * @param layout the GridLayout node
65      * @param view the view instance of the grid layout receiving the drop
66      */
GridDropHandler(GridLayoutRule gridLayoutRule, INode layout, Object view)67     public GridDropHandler(GridLayoutRule gridLayoutRule, INode layout, Object view) {
68         mRule = gridLayoutRule;
69         mGrid = GridModel.get(mRule.getRulesEngine(), layout, view);
70     }
71 
72     /**
73      * Computes the best horizontal and vertical matches for a drag to the given position.
74      *
75      * @param feedback a {@link DropFeedback} object containing drag state like the drag
76      *            bounds and the drag baseline
77      * @param p the mouse position
78      */
computeMatches(DropFeedback feedback, Point p)79     public void computeMatches(DropFeedback feedback, Point p) {
80         mRowMatch = mColumnMatch = null;
81         feedback.tooltip = null;
82 
83         Rect bounds = mGrid.layout.getBounds();
84         int x1 = p.x;
85         int y1 = p.y;
86 
87         Rect dragBounds = feedback.dragBounds;
88         int w = dragBounds != null ? dragBounds.w : 0;
89         int h = dragBounds != null ? dragBounds.h : 0;
90         if (!GridLayoutRule.sGridMode) {
91             if (dragBounds != null) {
92                 // Sometimes the items are centered under the mouse so
93                 // offset by the top left corner distance
94                 x1 += dragBounds.x;
95                 y1 += dragBounds.y;
96             }
97 
98             int x2 = x1 + w;
99             int y2 = y1 + h;
100 
101             if (x2 < bounds.x || y2 < bounds.y || x1 > bounds.x2() || y1 > bounds.y2()) {
102                 return;
103             }
104 
105             List<GridMatch> columnMatches = new ArrayList<GridMatch>();
106             List<GridMatch> rowMatches = new ArrayList<GridMatch>();
107             int max = BaseLayoutRule.getMaxMatchDistance();
108 
109             // Column matches:
110             addLeftSideMatch(x1, columnMatches, max);
111             addRightSideMatch(x2, columnMatches, max);
112             addCenterColumnMatch(bounds, x1, y1, x2, y2, columnMatches, max);
113 
114             // Row matches:
115             int row = (mGrid.getViewCount() == 0) ? 0 : mGrid.getClosestRow(y1);
116             int rowY = mGrid.getRowY(row);
117             addTopMatch(y1, rowMatches, max, row, rowY);
118             addBaselineMatch(feedback.dragBaseline, y1, rowMatches, max, row, rowY);
119             addBottomMatch(y2, rowMatches, max);
120 
121             // Look for gap-matches: Predefined spacing between widgets.
122             // TODO: Make this use metadata for predefined spacing between
123             // pairs of types of components. For example, buttons have certain
124             // inserts in their 9-patch files (depending on the theme) that should
125             // be considered and subtracted from the overall proposed distance!
126             addColumnGapMatch(bounds, x1, x2, columnMatches, max);
127             addRowGapMatch(bounds, y1, y2, rowMatches, max);
128 
129             // Fallback: Split existing cell. Also do snap-to-grid.
130             if (GridLayoutRule.sSnapToGrid) {
131                 x1 = ((x1 - MARGIN_SIZE - bounds.x) / GRID_SIZE) * GRID_SIZE
132                         + MARGIN_SIZE + bounds.x;
133                 y1 = ((y1 - MARGIN_SIZE - bounds.y) / GRID_SIZE) * GRID_SIZE
134                         + MARGIN_SIZE + bounds.y;
135                 x2 = x1 + w;
136                 y2 = y1 + h;
137             }
138 
139 
140             if (columnMatches.size() == 0 && x1 >= bounds.x) {
141                 // Split the current cell since we have no matches
142                 // TODO: Decide whether it should be gravity left or right...
143                 columnMatches.add(new GridMatch(SegmentType.LEFT, 0, x1, mGrid.getColumn(x1),
144                         true /* createCell */, UNDEFINED));
145             }
146             if (rowMatches.size() == 0 && y1 >= bounds.y) {
147                 rowMatches.add(new GridMatch(SegmentType.TOP, 0, y1, mGrid.getRow(y1),
148                         true /* createCell */, UNDEFINED));
149             }
150 
151             // Pick best matches
152             Collections.sort(rowMatches);
153             Collections.sort(columnMatches);
154 
155             mColumnMatch = null;
156             mRowMatch = null;
157             String columnDescription = null;
158             String rowDescription = null;
159             if (columnMatches.size() > 0) {
160                 mColumnMatch = columnMatches.get(0);
161                 columnDescription = mColumnMatch.getDisplayName(mGrid.layout);
162             }
163             if (rowMatches.size() > 0) {
164                 mRowMatch = rowMatches.get(0);
165                 rowDescription = mRowMatch.getDisplayName(mGrid.layout);
166             }
167 
168             if (columnDescription != null && rowDescription != null) {
169                 feedback.tooltip = columnDescription + '\n' + rowDescription;
170             }
171 
172             feedback.invalidTarget = mColumnMatch == null || mRowMatch == null;
173         } else {
174             // Find which cell we're inside.
175 
176             // TODO: Find out where within the cell we are, and offer to tweak the gravity
177             // based on the position.
178             int column = mGrid.getColumn(x1);
179             int row = mGrid.getRow(y1);
180 
181             int leftDistance = mGrid.getColumnDistance(column, x1);
182             int rightDistance = mGrid.getColumnDistance(column + 1, x1);
183             int topDistance = mGrid.getRowDistance(row, y1);
184             int bottomDistance = mGrid.getRowDistance(row + 1, y1);
185 
186             int SLOP = 2;
187             int radius = mRule.getNewCellSize();
188             if (rightDistance < radius + SLOP) {
189                 column = Math.min(column + 1, mGrid.actualColumnCount);
190                 leftDistance = rightDistance;
191             }
192             if (bottomDistance < radius + SLOP) {
193                 row = Math.min(row + 1, mGrid.actualRowCount);
194                 topDistance = bottomDistance;
195             }
196 
197             boolean createColumn = leftDistance < radius + SLOP;
198             boolean createRow = topDistance < radius + SLOP;
199             if (x1 >= bounds.x2()) {
200                 createColumn = true;
201             }
202             if (y1 >= bounds.y2()) {
203                 createRow = true;
204             }
205 
206             int cellWidth = leftDistance + rightDistance;
207             int cellHeight = topDistance + bottomDistance;
208             SegmentType horizontalType = SegmentType.LEFT;
209             SegmentType verticalType = SegmentType.TOP;
210             int minDistance = 10; // Don't center or right/bottom align in tiny cells
211             if (!createColumn && leftDistance > minDistance
212                     && dragBounds != null && dragBounds.w < cellWidth - 10) {
213                 if (rightDistance < leftDistance) {
214                     horizontalType = SegmentType.RIGHT;
215                 }
216 
217                 int centerDistance = Math.abs(cellWidth / 2 - leftDistance);
218                 if (centerDistance < leftDistance / 2 && centerDistance < rightDistance / 2) {
219                     horizontalType = SegmentType.CENTER_HORIZONTAL;
220                 }
221             }
222             if (!createRow && topDistance > minDistance
223                     && dragBounds != null && dragBounds.h < cellHeight - 10) {
224                 if (bottomDistance < topDistance) {
225                     verticalType = SegmentType.BOTTOM;
226                 }
227                 int centerDistance = Math.abs(cellHeight / 2 - topDistance);
228                 if (centerDistance < topDistance / 2 && centerDistance < bottomDistance / 2) {
229                     verticalType = SegmentType.CENTER_VERTICAL;
230                 }
231             }
232 
233             mColumnMatch = new GridMatch(horizontalType, 0, x1, column, createColumn, 0);
234             mRowMatch = new GridMatch(verticalType, 0, y1, row, createRow, 0);
235 
236             StringBuilder description = new StringBuilder(50);
237             String rowString = Integer.toString(mColumnMatch.cellIndex + 1);
238             String columnString = Integer.toString(mRowMatch.cellIndex + 1);
239             if (mRowMatch.createCell && mRowMatch.cellIndex < mGrid.actualRowCount) {
240                 description.append(String.format("Shift row %1$d down", mRowMatch.cellIndex + 1));
241                 description.append('\n');
242             }
243             if (mColumnMatch.createCell && mColumnMatch.cellIndex < mGrid.actualColumnCount) {
244                 description.append(String.format("Shift column %1$d right",
245                         mColumnMatch.cellIndex + 1));
246                 description.append('\n');
247             }
248             description.append(String.format("Insert into cell (%1$s,%2$s)",
249                     rowString, columnString));
250             description.append('\n');
251             description.append(String.format("Align %1$s, %2$s",
252                     horizontalType.name().toLowerCase(Locale.US),
253                     verticalType.name().toLowerCase(Locale.US)));
254             feedback.tooltip = description.toString();
255         }
256     }
257 
258     /**
259      * Adds a match to align the left edge with some other edge.
260      */
addLeftSideMatch(int x1, List<GridMatch> columnMatches, int max)261     private void addLeftSideMatch(int x1, List<GridMatch> columnMatches, int max) {
262         int column = (mGrid.getViewCount() == 0) ? 0 : mGrid.getClosestColumn(x1);
263         int columnX = mGrid.getColumnX(column);
264         int distance = abs(columnX - x1);
265         if (distance <= max) {
266             columnMatches.add(new GridMatch(SegmentType.LEFT, distance, columnX, column,
267                     false, UNDEFINED));
268         }
269     }
270 
271     /**
272      * Adds a match to align the right edge with some other edge.
273      */
addRightSideMatch(int x2, List<GridMatch> columnMatches, int max)274     private void addRightSideMatch(int x2, List<GridMatch> columnMatches, int max) {
275         // TODO: Only match the right hand side if the drag bounds fit fully within the
276         // cell! Ditto for match below.
277         int columnRight = (mGrid.getViewCount() == 0) ? 0 : mGrid.getClosestColumn(x2);
278         int rightDistance = mGrid.getColumnDistance(columnRight, x2);
279         if (rightDistance < max) {
280             int columnX = mGrid.getColumnX(columnRight);
281             if (columnX > mGrid.layout.getBounds().x) {
282                 columnMatches.add(new GridMatch(SegmentType.RIGHT, rightDistance, columnX,
283                         columnRight, false, UNDEFINED));
284             }
285         }
286     }
287 
288     /**
289      * Adds a horizontal match with the center axis of the GridLayout
290      */
addCenterColumnMatch(Rect bounds, int x1, int y1, int x2, int y2, List<GridMatch> columnMatches, int max)291     private void addCenterColumnMatch(Rect bounds, int x1, int y1, int x2, int y2,
292             List<GridMatch> columnMatches, int max) {
293         Collection<INode> intersectsRow = mGrid.getIntersectsRow(y1, y2);
294         if (intersectsRow.size() == 0) {
295             // Offer centering on this row since there isn't anything there
296             int matchedLine = bounds.centerX();
297             int distance = abs((x1 + x2) / 2 - matchedLine);
298             if (distance <= 2 * max) {
299                 boolean createCell = false; // always just put in column 0
300                 columnMatches.add(new GridMatch(SegmentType.CENTER_HORIZONTAL, distance,
301                         matchedLine, 0 /* column */, createCell, UNDEFINED));
302             }
303         }
304     }
305 
306     /**
307      * Adds a match to align the top edge with some other edge.
308      */
addTopMatch(int y1, List<GridMatch> rowMatches, int max, int row, int rowY)309     private void addTopMatch(int y1, List<GridMatch> rowMatches, int max, int row, int rowY) {
310         int distance = mGrid.getRowDistance(row, y1);
311         if (distance <= max) {
312             rowMatches.add(new GridMatch(SegmentType.TOP, distance, rowY, row, false,
313                     UNDEFINED));
314         }
315     }
316 
317     /**
318      * Adds a match to align the bottom edge with some other edge.
319      */
addBottomMatch(int y2, List<GridMatch> rowMatches, int max)320     private void addBottomMatch(int y2, List<GridMatch> rowMatches, int max) {
321         int rowBottom = (mGrid.getViewCount() == 0) ? 0 : mGrid.getClosestRow(y2);
322         int distance = mGrid.getRowDistance(rowBottom, y2);
323         if (distance < max) {
324             int rowY = mGrid.getRowY(rowBottom);
325             if (rowY > mGrid.layout.getBounds().y) {
326                 rowMatches.add(new GridMatch(SegmentType.BOTTOM, distance, rowY,
327                         rowBottom, false, UNDEFINED));
328             }
329         }
330     }
331 
332     /**
333      * Adds a baseline match, if applicable.
334      */
addBaselineMatch(int dragBaseline, int y1, List<GridMatch> rowMatches, int max, int row, int rowY)335     private void addBaselineMatch(int dragBaseline, int y1, List<GridMatch> rowMatches, int max,
336             int row, int rowY) {
337         int dragBaselineY = y1 + dragBaseline;
338         int rowBaseline = mGrid.getBaseline(row);
339         if (rowBaseline != -1) {
340             int rowBaselineY = rowY + rowBaseline;
341             int distance = abs(dragBaselineY - rowBaselineY);
342             if (distance < max) {
343                 rowMatches.add(new GridMatch(SegmentType.BASELINE, distance, rowBaselineY, row,
344                         false, UNDEFINED));
345             }
346         }
347     }
348 
349     /**
350      * Computes a horizontal "gap" match - a preferred distance from the nearest edge,
351      * including margin edges
352      */
addColumnGapMatch(Rect bounds, int x1, int x2, List<GridMatch> columnMatches, int max)353     private void addColumnGapMatch(Rect bounds, int x1, int x2, List<GridMatch> columnMatches,
354             int max) {
355         if (x1 < bounds.x + MARGIN_SIZE + max) {
356             int matchedLine = bounds.x + MARGIN_SIZE;
357             int distance = abs(matchedLine - x1);
358             if (distance <= max) {
359                 boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine;
360                 columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine,
361                         0, createCell, MARGIN_SIZE));
362             }
363         } else if (x2 > bounds.x2() - MARGIN_SIZE - max) {
364             int matchedLine = bounds.x2() - MARGIN_SIZE;
365             int distance = abs(matchedLine - x2);
366             if (distance <= max) {
367                 // This does not yet work properly; we need to use columnWeights to achieve this
368                 //boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine;
369                 //columnMatches.add(new GridMatch(SegmentType.RIGHT, distance, matchedLine,
370                 //        mGrid.actualColumnCount - 1, createCell, MARGIN_SIZE));
371             }
372         } else {
373             int columnRight = mGrid.getColumn(x1 - SHORT_GAP_DP);
374             int columnX = mGrid.getColumnMaxX(columnRight);
375             int matchedLine = columnX + SHORT_GAP_DP;
376             int distance = abs(matchedLine - x1);
377             if (distance <= max) {
378                 boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine;
379                 columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine,
380                         columnRight, createCell, SHORT_GAP_DP));
381             }
382 
383             // Add a column directly adjacent (no gap)
384             columnRight = mGrid.getColumn(x1);
385             columnX = mGrid.getColumnMaxX(columnRight);
386             matchedLine = columnX;
387             distance = abs(matchedLine - x1);
388 
389             // Let's say you have this arrangement:
390             //     [button1][button2]
391             // This is two columns, where the right hand side edge of column 1 is
392             // flush with the left side edge of column 2, because in fact the width of
393             // button1 is what defines the width of column 1, and that in turn is what
394             // defines the left side position of column 2.
395             //
396             // In this case we don't want to consider inserting a new column at the
397             // right hand side of button1 a better match than matching left on column 2.
398             // Therefore, to ensure that this doesn't happen, we "penalize" right column
399             // matches such that they don't get preferential treatment when the matching
400             // line is on the left side of the column.
401             distance += 2;
402 
403             if (distance <= max) {
404                 boolean createCell = mGrid.getColumnX(mGrid.getColumn(matchedLine)) != matchedLine;
405                 columnMatches.add(new GridMatch(SegmentType.LEFT, distance, matchedLine,
406                         columnRight, createCell, 0));
407             }
408         }
409     }
410 
411     /**
412      * Computes a vertical "gap" match - a preferred distance from the nearest edge,
413      * including margin edges
414      */
addRowGapMatch(Rect bounds, int y1, int y2, List<GridMatch> rowMatches, int max)415     private void addRowGapMatch(Rect bounds, int y1, int y2, List<GridMatch> rowMatches, int max) {
416         if (y1 < bounds.y + MARGIN_SIZE + max) {
417             int matchedLine = bounds.y + MARGIN_SIZE;
418             int distance = abs(matchedLine - y1);
419             if (distance <= max) {
420                 boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine;
421                 rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine,
422                         0, createCell, MARGIN_SIZE));
423             }
424         } else if (y2 > bounds.y2() - MARGIN_SIZE - max) {
425             int matchedLine = bounds.y2() - MARGIN_SIZE;
426             int distance = abs(matchedLine - y2);
427             if (distance <= max) {
428                 // This does not yet work properly; we need to use columnWeights to achieve this
429                 //boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine;
430                 //rowMatches.add(new GridMatch(SegmentType.BOTTOM, distance, matchedLine,
431                 //        mGrid.actualRowCount - 1, createCell, MARGIN_SIZE));
432             }
433         } else {
434             int rowBottom = mGrid.getRow(y1 - SHORT_GAP_DP);
435             int rowY = mGrid.getRowMaxY(rowBottom);
436             int matchedLine = rowY + SHORT_GAP_DP;
437             int distance = abs(matchedLine - y1);
438             if (distance <= max) {
439                 boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine;
440                 rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine,
441                         rowBottom, createCell, SHORT_GAP_DP));
442             }
443 
444             // Add a row directly adjacent (no gap)
445             rowBottom = mGrid.getRow(y1);
446             rowY = mGrid.getRowMaxY(rowBottom);
447             matchedLine = rowY;
448             distance = abs(matchedLine - y1);
449             distance += 2; // See explanation in addColumnGapMatch
450             if (distance <= max) {
451                 boolean createCell = mGrid.getRowY(mGrid.getRow(matchedLine)) != matchedLine;
452                 rowMatches.add(new GridMatch(SegmentType.TOP, distance, matchedLine,
453                         rowBottom, createCell, 0));
454             }
455 
456         }
457     }
458 
459     /**
460      * Called when a node is dropped in free-form mode. This will insert the dragged
461      * element into the grid and returns the newly created node.
462      *
463      * @param targetNode the GridLayout node
464      * @param element the dragged element
465      * @return the newly created {@link INode}
466      */
handleFreeFormDrop(INode targetNode, IDragElement element)467     public INode handleFreeFormDrop(INode targetNode, IDragElement element) {
468         assert mRowMatch != null;
469         assert mColumnMatch != null;
470 
471         String fqcn = element.getFqcn();
472 
473         INode newChild = null;
474 
475         Rect bounds = element.getBounds();
476         int row = mRowMatch.cellIndex;
477         int column = mColumnMatch.cellIndex;
478 
479         if (targetNode.getChildren().length == 0) {
480             //
481             // Set up the initial structure:
482             //
483             //
484             //    Fixed                                 Fixed
485             //     Size                                  Size
486             //    Column       Expanding Column         Column
487             //   +-----+-------------------------------+-----+
488             //   |     |                               |     |
489             //   | 0,0 |              0,1              | 0,2 | Fixed Size Row
490             //   |     |                               |     |
491             //   +-----+-------------------------------+-----+
492             //   |     |                               |     |
493             //   |     |                               |     |
494             //   |     |                               |     |
495             //   | 1,0 |              1,1              | 1,2 | Expanding Row
496             //   |     |                               |     |
497             //   |     |                               |     |
498             //   |     |                               |     |
499             //   +-----+-------------------------------+-----+
500             //   |     |                               |     |
501             //   | 2,0 |              2,1              | 2,2 | Fixed Size Row
502             //   |     |                               |     |
503             //   +-----+-------------------------------+-----+
504             //
505             // This is implemented in GridLayout by the following grid, where
506             // SC1 has columnWeight=1 and SR1 has rowWeight=1.
507             // (SC=Space for Column, SR=Space for Row)
508             //
509             //   +------+-------------------------------+------+
510             //   |      |                               |      |
511             //   | SCR0 |             SC1               | SC2  |
512             //   |      |                               |      |
513             //   +------+-------------------------------+------+
514             //   |      |                               |      |
515             //   |      |                               |      |
516             //   |      |                               |      |
517             //   | SR1  |                               |      |
518             //   |      |                               |      |
519             //   |      |                               |      |
520             //   |      |                               |      |
521             //   +------+-------------------------------+------+
522             //   |      |                               |      |
523             //   | SR2  |                               |      |
524             //   |      |                               |      |
525             //   +------+-------------------------------+------+
526             //
527             // Note that when we split columns and rows here, if splitting the expanding
528             // row or column then the row or column weight should be moved to the right or
529             // bottom half!
530 
531 
532             //int columnX = mGrid.getColumnX(column);
533             //int rowY = mGrid.getRowY(row);
534 
535             mGrid.setGridAttribute(targetNode, ATTR_COLUMN_COUNT, 2);
536             //mGrid.setGridAttribute(targetNode, ATTR_COLUMN_COUNT, 3);
537             //INode scr0 = addSpacer(targetNode, -1, 0, 0, 1, 1);
538             //INode sc1 = addSpacer(targetNode, -1, 0, 1, 0, 0);
539             //INode sc2 = addSpacer(targetNode, -1, 0, 2, 1, 0);
540             //INode sr1 = addSpacer(targetNode, -1, 1, 0, 0, 0);
541             //INode sr2 = addSpacer(targetNode, -1, 2, 0, 0, 1);
542             //mGrid.setGridAttribute(sc1, ATTR_LAYOUT_GRAVITY, VALUE_FILL_HORIZONTAL);
543             //mGrid.setGridAttribute(sr1, ATTR_LAYOUT_GRAVITY, VALUE_FILL_VERTICAL);
544             //
545             //mGrid.loadFromXml();
546             //column = mGrid.getColumn(columnX);
547             //row = mGrid.getRow(rowY);
548         }
549 
550         int startX, endX;
551         if (mColumnMatch.type == SegmentType.RIGHT) {
552             endX = mColumnMatch.matchedLine - 1;
553             startX = endX - bounds.w;
554             column = mGrid.getColumn(startX);
555         } else {
556             startX = mColumnMatch.matchedLine; // TODO: What happens on type=RIGHT?
557             endX = startX + bounds.w;
558         }
559         int startY, endY;
560         if (mRowMatch.type == SegmentType.BOTTOM) {
561             endY = mRowMatch.matchedLine - 1;
562             startY = endY - bounds.h;
563             row = mGrid.getRow(startY);
564         } else if (mRowMatch.type == SegmentType.BASELINE) {
565             // TODO: The rowSpan should always be 1 for baseline alignments, since
566             // otherwise the alignment won't work!
567             startY = endY = mRowMatch.matchedLine;
568         } else {
569             startY = mRowMatch.matchedLine;
570             endY = startY + bounds.h;
571         }
572         int endColumn = mGrid.getColumn(endX);
573         int endRow = mGrid.getRow(endY);
574         int columnSpan = endColumn - column + 1;
575         int rowSpan = endRow - row + 1;
576 
577         // Make sure my math was right:
578         assert mRowMatch.type != SegmentType.BASELINE || rowSpan == 1 : rowSpan;
579 
580         // If the item almost fits into the row (at most N % bigger) then just enlarge
581         // the row; don't add a rowspan since that will defeat baseline alignment etc
582         if (!mRowMatch.createCell && bounds.h <= MAX_CELL_DIFFERENCE * mGrid.getRowHeight(
583                 mRowMatch.type == SegmentType.BOTTOM ? endRow : row, 1)) {
584             if (mRowMatch.type == SegmentType.BOTTOM) {
585                 row += rowSpan - 1;
586             }
587             rowSpan = 1;
588         }
589         if (!mColumnMatch.createCell && bounds.w <= MAX_CELL_DIFFERENCE * mGrid.getColumnWidth(
590                 mColumnMatch.type == SegmentType.RIGHT ? endColumn : column, 1)) {
591             if (mColumnMatch.type == SegmentType.RIGHT) {
592                 column += columnSpan - 1;
593             }
594             columnSpan = 1;
595         }
596 
597         if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) {
598             column = 0;
599             columnSpan = mGrid.actualColumnCount;
600         }
601 
602         // Temporary: Ensure we don't get in trouble with implicit positions
603         mGrid.applyPositionAttributes();
604 
605         // Split cells to make a new column
606         if (mColumnMatch.createCell) {
607             int columnWidthPx = mGrid.getColumnDistance(column, mColumnMatch.matchedLine);
608             //assert columnWidthPx == columnMatch.distance; // TBD? IF so simplify
609             int columnWidthDp = mRule.getRulesEngine().pxToDp(columnWidthPx);
610 
611             int maxX = mGrid.getColumnMaxX(column);
612             boolean insertMarginColumn = false;
613             if (mColumnMatch.margin == 0) {
614                 columnWidthDp = 0;
615             } else if (mColumnMatch.margin != UNDEFINED) {
616                 int distance = abs(mColumnMatch.matchedLine - (maxX + mColumnMatch.margin));
617                 insertMarginColumn = column > 0 && distance < 2;
618                 if (insertMarginColumn) {
619                     int margin = mColumnMatch.margin;
620                     if (ViewMetadataRepository.INSETS_SUPPORTED) {
621                         IViewMetadata metadata = mRule.getRulesEngine().getMetadata(fqcn);
622                         if (metadata != null) {
623                             Margins insets = metadata.getInsets();
624                             if (insets != null) {
625                                 // TODO:
626                                 // Consider left or right side attachment
627                                 // TODO: Also consider inset of element on cell to the left
628                                 margin -= insets.left;
629                             }
630                         }
631                     }
632 
633                     columnWidthDp = mRule.getRulesEngine().pxToDp(margin);
634                 }
635             }
636 
637             column++;
638             mGrid.splitColumn(column, insertMarginColumn, columnWidthDp, mColumnMatch.matchedLine);
639             if (insertMarginColumn) {
640                 column++;
641             }
642         }
643 
644         // Split cells to make a new  row
645         if (mRowMatch.createCell) {
646             int rowHeightPx = mGrid.getRowDistance(row, mRowMatch.matchedLine);
647             //assert rowHeightPx == rowMatch.distance; // TBD? If so simplify
648             int rowHeightDp = mRule.getRulesEngine().pxToDp(rowHeightPx);
649 
650             int maxY = mGrid.getRowMaxY(row);
651             boolean insertMarginRow = false;
652             if (mRowMatch.margin == 0) {
653                 rowHeightDp = 0;
654             } else if (mRowMatch.margin != UNDEFINED) {
655                 int distance = abs(mRowMatch.matchedLine - (maxY + mRowMatch.margin));
656                 insertMarginRow = row > 0 && distance < 2;
657                 if (insertMarginRow) {
658                     int margin = mRowMatch.margin;
659                     IViewMetadata metadata = mRule.getRulesEngine().getMetadata(element.getFqcn());
660                     if (metadata != null) {
661                         Margins insets = metadata.getInsets();
662                         if (insets != null) {
663                             // TODO:
664                             // Consider left or right side attachment
665                             // TODO: Also consider inset of element on cell to the left
666                             margin -= insets.top;
667                         }
668                     }
669 
670                     rowHeightDp = mRule.getRulesEngine().pxToDp(margin);
671                 }
672             }
673 
674             row++;
675             mGrid.splitRow(row, insertMarginRow, rowHeightDp, mRowMatch.matchedLine);
676             if (insertMarginRow) {
677                 row++;
678             }
679         }
680 
681         // Figure out where to insert the new child
682 
683         int index = mGrid.getInsertIndex(row, column);
684         if (index == -1) {
685             // Couldn't find a later place to insert
686             newChild = targetNode.appendChild(fqcn);
687         } else {
688             GridModel.ViewData next = mGrid.getView(index);
689 
690             newChild = targetNode.insertChildAt(fqcn, index);
691 
692             // Must also apply positions to the following child to ensure
693             // that the new child doesn't affect the implicit numbering!
694             // TODO: We can later check whether the implied number is equal to
695             // what it already is such that we don't need this
696             next.applyPositionAttributes();
697         }
698 
699         // Set the cell position (gravity) of the new widget
700         int gravity = 0;
701         if (mColumnMatch.type == SegmentType.RIGHT) {
702             gravity |= GravityHelper.GRAVITY_RIGHT;
703         } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) {
704             gravity |= GravityHelper.GRAVITY_CENTER_HORIZ;
705         }
706         mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, column);
707         if (mRowMatch.type == SegmentType.BASELINE) {
708             // There *is* no baseline gravity constant, instead, leave the
709             // vertical gravity unspecified and GridLayout will treat it as
710             // baseline alignment
711             //gravity |= GravityHelper.GRAVITY_BASELINE;
712         } else if (mRowMatch.type == SegmentType.BOTTOM) {
713             gravity |= GravityHelper.GRAVITY_BOTTOM;
714         } else if (mRowMatch.type == SegmentType.CENTER_VERTICAL) {
715             gravity |= GravityHelper.GRAVITY_CENTER_VERT;
716         }
717         // Ensure that we have at least one horizontal and vertical constraint, otherwise
718         // the new item will be fixed. As an example, if we have a single button in the
719         // table which we inserted *without* a gravity, and we then insert a button
720         // above it with a vertical gravity, then only the top column would be considered
721         // stretchable, and it will fill all available vertical space and the previous
722         // button will jump to the bottom.
723         if (!GravityHelper.isConstrainedHorizontally(gravity)) {
724             gravity |= GravityHelper.GRAVITY_LEFT;
725         }
726         /* This causes problems: Try placing two buttons vertically from the top of the layout.
727            We need to solve the free column/free row problem first.
728         if (!GravityHelper.isConstrainedVertically(gravity)
729                 // There is no baseline constant, so we have to leave it unconstrained instead
730                 && mRowMatch.type != SegmentType.BASELINE
731                 // You also can't baseline align one element with another that has vertical
732                 // alignment top or bottom, so when we first "freely" place views (e.g.
733                 // at a particular y location), also place it freely (no constraint).
734                 && !mRowMatch.createCell) {
735             gravity |= GravityHelper.GRAVITY_TOP;
736         }
737         */
738         mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, getGravity(gravity));
739 
740         mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, row);
741 
742         // Apply spans to ensure that the widget can fit without pushing columns
743         if (columnSpan > 1) {
744             mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN_SPAN, columnSpan);
745         }
746         if (rowSpan > 1) {
747             mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW_SPAN, rowSpan);
748         }
749 
750         // Ensure that we don't store columnCount=0
751         if (mGrid.actualColumnCount == 0) {
752             mGrid.setGridAttribute(mGrid.layout, ATTR_COLUMN_COUNT, Math.max(1, column + 1));
753         }
754 
755         return newChild;
756     }
757 
758     /**
759      * Called when a drop is completed and we're in grid-editing mode. This will insert
760      * the dragged element into the target cell.
761      *
762      * @param targetNode the GridLayout node
763      * @param element the dragged element
764      * @return the newly created node
765      */
handleGridModeDrop(INode targetNode, IDragElement element)766     public INode handleGridModeDrop(INode targetNode, IDragElement element) {
767         String fqcn = element.getFqcn();
768         INode newChild = targetNode.appendChild(fqcn);
769 
770         int column = mColumnMatch.cellIndex;
771         if (mColumnMatch.createCell) {
772             mGrid.addColumn(column,
773                     newChild, UNDEFINED, false, UNDEFINED, UNDEFINED);
774         }
775         int row = mRowMatch.cellIndex;
776         if (mRowMatch.createCell) {
777             mGrid.addRow(row, newChild, UNDEFINED, false, UNDEFINED, UNDEFINED);
778         }
779 
780         mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, column);
781         mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, row);
782 
783         int gravity = 0;
784         if (mColumnMatch.type == SegmentType.RIGHT) {
785             gravity |= GravityHelper.GRAVITY_RIGHT;
786         } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) {
787             gravity |= GravityHelper.GRAVITY_CENTER_HORIZ;
788         }
789         if (mRowMatch.type == SegmentType.BASELINE) {
790             // There *is* no baseline gravity constant, instead, leave the
791             // vertical gravity unspecified and GridLayout will treat it as
792             // baseline alignment
793             //gravity |= GravityHelper.GRAVITY_BASELINE;
794         } else if (mRowMatch.type == SegmentType.BOTTOM) {
795             gravity |= GravityHelper.GRAVITY_BOTTOM;
796         } else if (mRowMatch.type == SegmentType.CENTER_VERTICAL) {
797             gravity |= GravityHelper.GRAVITY_CENTER_VERT;
798         }
799         if (!GravityHelper.isConstrainedHorizontally(gravity)) {
800             gravity |= GravityHelper.GRAVITY_LEFT;
801         }
802         if (!GravityHelper.isConstrainedVertically(gravity)) {
803             gravity |= GravityHelper.GRAVITY_TOP;
804         }
805         mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, getGravity(gravity));
806 
807         if (mGrid.declaredColumnCount == UNDEFINED || mGrid.declaredColumnCount < column + 1) {
808             mGrid.setGridAttribute(mGrid.layout, ATTR_COLUMN_COUNT, column + 1);
809         }
810 
811         return newChild;
812     }
813 
814     /**
815      * Returns the best horizontal match
816      *
817      * @return the best horizontal match, or null if there is no match
818      */
getColumnMatch()819     public GridMatch getColumnMatch() {
820         return mColumnMatch;
821     }
822 
823     /**
824      * Returns the best vertical match
825      *
826      * @return the best vertical match, or null if there is no match
827      */
getRowMatch()828     public GridMatch getRowMatch() {
829         return mRowMatch;
830     }
831 
832     /**
833      * Returns the grid used by the drop handler
834      *
835      * @return the grid used by the drop handler, never null
836      */
getGrid()837     public GridModel getGrid() {
838         return mGrid;
839     }
840 }
841