Home | History | Annotate | Download | only in grid
      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      */
     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      */
     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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    837     public GridModel getGrid() {
    838         return mGrid;
    839     }
    840 }
    841