public void testDropTopEdge() {
    // If we drag right into the button itself, not a valid drop position
    INode inserted =
        dragInto(
            new Rect(0, 0, 105, 80),
            new Point(30, -10),
            null,
            2,
            -1,
            // Bounds rectangle
            "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])",

            // Preview line + drop zone rectangle along the top
            "useStyle(DROP_ZONE), drawRect(Rect[0,-10,240,20])",
            "useStyle(DROP_ZONE_ACTIVE), fillRect(Rect[0,-10,240,20])",
            "useStyle(DROP_PREVIEW), drawLine(0,0,240,0)",

            // Tip
            "useStyle(HELP), drawBoxedStrings(5,15,[alignParentTop])",

            // Drop preview
            "useStyle(DROP_PREVIEW), drawRect(Rect[0,0,105,80])");

    assertEquals("true", inserted.getStringAttr(ANDROID_URI, "layout_alignParentTop"));
  }
  @Override
  public void onCreate(@NonNull INode node, @NonNull INode parent, @NonNull InsertType insertType) {
    super.onCreate(node, parent, insertType);

    if (insertType.isCreate()) {
      for (int i = 0; i < 3; i++) {
        INode handle = node.appendChild(SdkConstants.FQCN_RADIO_BUTTON);
        handle.setAttribute(ANDROID_URI, ATTR_ID, String.format("@+id/radio%d", i));
        if (i == 0) {
          handle.setAttribute(ANDROID_URI, ATTR_CHECKED, VALUE_TRUE);
        }
      }
    }
  }
  /**
   * Called when a drop is completed and we're in grid-editing mode. This will insert the dragged
   * element into the target cell.
   *
   * @param targetNode the GridLayout node
   * @param element the dragged element
   * @return the newly created node
   */
  public INode handleGridModeDrop(INode targetNode, IDragElement element) {
    String fqcn = element.getFqcn();
    INode newChild = targetNode.appendChild(fqcn);

    int column = mColumnMatch.cellIndex;
    if (mColumnMatch.createCell) {
      mGrid.addColumn(column, newChild, UNDEFINED, false, UNDEFINED, UNDEFINED);
    }
    int row = mRowMatch.cellIndex;
    if (mRowMatch.createCell) {
      mGrid.addRow(row, newChild, UNDEFINED, false, UNDEFINED, UNDEFINED);
    }

    mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, column);
    mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, row);

    int gravity = 0;
    if (mColumnMatch.type == SegmentType.RIGHT) {
      gravity |= GravityHelper.GRAVITY_RIGHT;
    } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) {
      gravity |= GravityHelper.GRAVITY_CENTER_HORIZ;
    }
    if (mRowMatch.type == SegmentType.BASELINE) {
      // There *is* no baseline gravity constant, instead, leave the
      // vertical gravity unspecified and GridLayout will treat it as
      // baseline alignment
      // gravity |= GravityHelper.GRAVITY_BASELINE;
    } else if (mRowMatch.type == SegmentType.BOTTOM) {
      gravity |= GravityHelper.GRAVITY_BOTTOM;
    } else if (mRowMatch.type == SegmentType.CENTER_VERTICAL) {
      gravity |= GravityHelper.GRAVITY_CENTER_VERT;
    }
    if (!GravityHelper.isConstrainedHorizontally(gravity)) {
      gravity |= GravityHelper.GRAVITY_LEFT;
    }
    if (!GravityHelper.isConstrainedVertically(gravity)) {
      gravity |= GravityHelper.GRAVITY_TOP;
    }
    mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, getGravity(gravity));

    if (mGrid.declaredColumnCount == UNDEFINED || mGrid.declaredColumnCount < column + 1) {
      mGrid.setGridAttribute(mGrid.layout, ATTR_COLUMN_COUNT, column + 1);
    }

    return newChild;
  }
  public void testDropZones() {
    List<Pair<Point, String[]>> zones = new ArrayList<Pair<Point, String[]>>();

    zones.add(
        Pair.of(
            new Point(51 + 10, 181 + 10),
            new String[] {"above=@+id/Centered", "toLeftOf=@+id/Centered"}));
    zones.add(
        Pair.of(
            new Point(71 + 10, 181 + 10),
            new String[] {"above=@+id/Centered", "alignLeft=@+id/Centered"}));
    zones.add(
        Pair.of(
            new Point(104 + 10, 181 + 10),
            new String[] {"above=@+id/Centered", "alignRight=@+id/Centered"}));
    zones.add(
        Pair.of(
            new Point(137 + 10, 181 + 10),
            new String[] {"above=@+id/Centered", "alignRight=@+id/Centered"}));
    zones.add(
        Pair.of(
            new Point(170 + 10, 181 + 10),
            new String[] {"above=@+id/Centered", "toRightOf=@+id/Centered"}));
    zones.add(
        Pair.of(
            new Point(51 + 10, 279 + 10),
            new String[] {"below=@+id/Centered", "toLeftOf=@+id/Centered"}));
    zones.add(
        Pair.of(
            new Point(71 + 10, 279 + 10),
            new String[] {"below=@+id/Centered", "alignLeft=@+id/Centered"}));
    zones.add(
        Pair.of(
            new Point(104 + 10, 279 + 10),
            new String[] {"below=@+id/Centered", "alignLeft=@+id/Centered"}));
    zones.add(
        Pair.of(
            new Point(137 + 10, 279 + 10),
            new String[] {"below=@+id/Centered", "alignRight=@+id/Centered"}));
    zones.add(
        Pair.of(
            new Point(170 + 10, 279 + 10),
            new String[] {"below=@+id/Centered", "toRightOf=@+id/Centered"}));
    zones.add(
        Pair.of(
            new Point(51 + 10, 201 + 10),
            new String[] {"toLeftOf=@+id/Centered", "alignTop=@+id/Centered"}));
    zones.add(
        Pair.of(
            new Point(51 + 10, 227 + 10),
            new String[] {"toLeftOf=@+id/Centered", "alignTop=@+id/Centered"}));
    zones.add(
        Pair.of(
            new Point(170 + 10, 201 + 10),
            new String[] {"toRightOf=@+id/Centered", "alignTop=@+id/Centered"}));
    zones.add(
        Pair.of(
            new Point(51 + 10, 253 + 10),
            new String[] {"toLeftOf=@+id/Centered", "alignBottom=@+id/Centered"}));
    zones.add(
        Pair.of(
            new Point(170 + 10, 227 + 10),
            new String[] {
              "toRightOf=@+id/Centered", "alignTop=@+id/Centered", "alignBottom=@+id/Centered"
            }));
    zones.add(
        Pair.of(
            new Point(170 + 10, 253 + 10),
            new String[] {"toRightOf=@+id/Centered", "alignBottom=@+id/Centered"}));

    for (Pair<Point, String[]> zonePair : zones) {
      Point dropPoint = zonePair.getFirst();
      String[] attachments = zonePair.getSecond();
      // If we drag right into the button itself, not a valid drop position

      INode inserted =
          dragInto(
              new Rect(0, 0, 105, 80),
              new Point(120, 240),
              dropPoint,
              1,
              -1,
              attachments,

              // Bounds rectangle
              "useStyle(DROP_RECIPIENT), drawRect(Rect[0,0,240,480])",

              // Drop zones
              "useStyle(DROP_ZONE), "
                  + "drawRect(Rect[51,181,20,20]), drawRect(Rect[71,181,33,20]), "
                  + "drawRect(Rect[104,181,33,20]), drawRect(Rect[137,181,33,20]), "
                  + "drawRect(Rect[170,181,20,20]), drawRect(Rect[51,279,20,20]), "
                  + "drawRect(Rect[71,279,33,20]), drawRect(Rect[104,279,33,20]), "
                  + "drawRect(Rect[137,279,33,20]), drawRect(Rect[170,279,20,20]), "
                  + "drawRect(Rect[51,201,20,26]), drawRect(Rect[51,227,20,26]), "
                  + "drawRect(Rect[51,253,20,26]), drawRect(Rect[170,201,20,26]), "
                  + "drawRect(Rect[170,227,20,26]), drawRect(Rect[170,253,20,26])");

      for (String attachment : attachments) {
        String[] elements = attachment.split("=");
        String name = "layout_" + elements[0];
        String value = elements[1];
        assertEquals(value, inserted.getStringAttr(ANDROID_URI, name));
      }
    }
  }
  /**
   * Called when a node is dropped in free-form mode. This will insert the dragged element into the
   * grid and returns the newly created node.
   *
   * @param targetNode the GridLayout node
   * @param element the dragged element
   * @return the newly created {@link INode}
   */
  public INode handleFreeFormDrop(INode targetNode, IDragElement element) {
    assert mRowMatch != null;
    assert mColumnMatch != null;

    String fqcn = element.getFqcn();

    INode newChild = null;

    Rect bounds = element.getBounds();
    int row = mRowMatch.cellIndex;
    int column = mColumnMatch.cellIndex;

    if (targetNode.getChildren().length == 0) {
      //
      // Set up the initial structure:
      //
      //
      //    Fixed                                 Fixed
      //     Size                                  Size
      //    Column       Expanding Column         Column
      //   +-----+-------------------------------+-----+
      //   |     |                               |     |
      //   | 0,0 |              0,1              | 0,2 | Fixed Size Row
      //   |     |                               |     |
      //   +-----+-------------------------------+-----+
      //   |     |                               |     |
      //   |     |                               |     |
      //   |     |                               |     |
      //   | 1,0 |              1,1              | 1,2 | Expanding Row
      //   |     |                               |     |
      //   |     |                               |     |
      //   |     |                               |     |
      //   +-----+-------------------------------+-----+
      //   |     |                               |     |
      //   | 2,0 |              2,1              | 2,2 | Fixed Size Row
      //   |     |                               |     |
      //   +-----+-------------------------------+-----+
      //
      // This is implemented in GridLayout by the following grid, where
      // SC1 has columnWeight=1 and SR1 has rowWeight=1.
      // (SC=Space for Column, SR=Space for Row)
      //
      //   +------+-------------------------------+------+
      //   |      |                               |      |
      //   | SCR0 |             SC1               | SC2  |
      //   |      |                               |      |
      //   +------+-------------------------------+------+
      //   |      |                               |      |
      //   |      |                               |      |
      //   |      |                               |      |
      //   | SR1  |                               |      |
      //   |      |                               |      |
      //   |      |                               |      |
      //   |      |                               |      |
      //   +------+-------------------------------+------+
      //   |      |                               |      |
      //   | SR2  |                               |      |
      //   |      |                               |      |
      //   +------+-------------------------------+------+
      //
      // Note that when we split columns and rows here, if splitting the expanding
      // row or column then the row or column weight should be moved to the right or
      // bottom half!

      // int columnX = mGrid.getColumnX(column);
      // int rowY = mGrid.getRowY(row);

      mGrid.setGridAttribute(targetNode, ATTR_COLUMN_COUNT, 2);
      // mGrid.setGridAttribute(targetNode, ATTR_COLUMN_COUNT, 3);
      // INode scr0 = addSpacer(targetNode, -1, 0, 0, 1, 1);
      // INode sc1 = addSpacer(targetNode, -1, 0, 1, 0, 0);
      // INode sc2 = addSpacer(targetNode, -1, 0, 2, 1, 0);
      // INode sr1 = addSpacer(targetNode, -1, 1, 0, 0, 0);
      // INode sr2 = addSpacer(targetNode, -1, 2, 0, 0, 1);
      // mGrid.setGridAttribute(sc1, ATTR_LAYOUT_GRAVITY, VALUE_FILL_HORIZONTAL);
      // mGrid.setGridAttribute(sr1, ATTR_LAYOUT_GRAVITY, VALUE_FILL_VERTICAL);
      //
      // mGrid.loadFromXml();
      // column = mGrid.getColumn(columnX);
      // row = mGrid.getRow(rowY);
    }

    int startX, endX;
    if (mColumnMatch.type == SegmentType.RIGHT) {
      endX = mColumnMatch.matchedLine - 1;
      startX = endX - bounds.w;
      column = mGrid.getColumn(startX);
    } else {
      startX = mColumnMatch.matchedLine; // TODO: What happens on type=RIGHT?
      endX = startX + bounds.w;
    }
    int startY, endY;
    if (mRowMatch.type == SegmentType.BOTTOM) {
      endY = mRowMatch.matchedLine - 1;
      startY = endY - bounds.h;
      row = mGrid.getRow(startY);
    } else if (mRowMatch.type == SegmentType.BASELINE) {
      // TODO: The rowSpan should always be 1 for baseline alignments, since
      // otherwise the alignment won't work!
      startY = endY = mRowMatch.matchedLine;
    } else {
      startY = mRowMatch.matchedLine;
      endY = startY + bounds.h;
    }
    int endColumn = mGrid.getColumn(endX);
    int endRow = mGrid.getRow(endY);
    int columnSpan = endColumn - column + 1;
    int rowSpan = endRow - row + 1;

    // Make sure my math was right:
    assert mRowMatch.type != SegmentType.BASELINE || rowSpan == 1 : rowSpan;

    // If the item almost fits into the row (at most N % bigger) then just enlarge
    // the row; don't add a rowspan since that will defeat baseline alignment etc
    if (!mRowMatch.createCell
        && bounds.h
            <= MAX_CELL_DIFFERENCE
                * mGrid.getRowHeight(mRowMatch.type == SegmentType.BOTTOM ? endRow : row, 1)) {
      if (mRowMatch.type == SegmentType.BOTTOM) {
        row += rowSpan - 1;
      }
      rowSpan = 1;
    }
    if (!mColumnMatch.createCell
        && bounds.w
            <= MAX_CELL_DIFFERENCE
                * mGrid.getColumnWidth(
                    mColumnMatch.type == SegmentType.RIGHT ? endColumn : column, 1)) {
      if (mColumnMatch.type == SegmentType.RIGHT) {
        column += columnSpan - 1;
      }
      columnSpan = 1;
    }

    if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) {
      column = 0;
      columnSpan = mGrid.actualColumnCount;
    }

    // Temporary: Ensure we don't get in trouble with implicit positions
    mGrid.applyPositionAttributes();

    // Split cells to make a new column
    if (mColumnMatch.createCell) {
      int columnWidthPx = mGrid.getColumnDistance(column, mColumnMatch.matchedLine);
      // assert columnWidthPx == columnMatch.distance; // TBD? IF so simplify
      int columnWidthDp = mRule.getRulesEngine().pxToDp(columnWidthPx);

      int maxX = mGrid.getColumnMaxX(column);
      boolean insertMarginColumn = false;
      if (mColumnMatch.margin == 0) {
        columnWidthDp = 0;
      } else if (mColumnMatch.margin != UNDEFINED) {
        int distance = abs(mColumnMatch.matchedLine - (maxX + mColumnMatch.margin));
        insertMarginColumn = column > 0 && distance < 2;
        if (insertMarginColumn) {
          int margin = mColumnMatch.margin;
          if (ViewMetadataRepository.INSETS_SUPPORTED) {
            IViewMetadata metadata = mRule.getRulesEngine().getMetadata(fqcn);
            if (metadata != null) {
              Margins insets = metadata.getInsets();
              if (insets != null) {
                // TODO:
                // Consider left or right side attachment
                // TODO: Also consider inset of element on cell to the left
                margin -= insets.left;
              }
            }
          }

          columnWidthDp = mRule.getRulesEngine().pxToDp(margin);
        }
      }

      column++;
      mGrid.splitColumn(column, insertMarginColumn, columnWidthDp, mColumnMatch.matchedLine);
      if (insertMarginColumn) {
        column++;
      }
    }

    // Split cells to make a new  row
    if (mRowMatch.createCell) {
      int rowHeightPx = mGrid.getRowDistance(row, mRowMatch.matchedLine);
      // assert rowHeightPx == rowMatch.distance; // TBD? If so simplify
      int rowHeightDp = mRule.getRulesEngine().pxToDp(rowHeightPx);

      int maxY = mGrid.getRowMaxY(row);
      boolean insertMarginRow = false;
      if (mRowMatch.margin == 0) {
        rowHeightDp = 0;
      } else if (mRowMatch.margin != UNDEFINED) {
        int distance = abs(mRowMatch.matchedLine - (maxY + mRowMatch.margin));
        insertMarginRow = row > 0 && distance < 2;
        if (insertMarginRow) {
          int margin = mRowMatch.margin;
          IViewMetadata metadata = mRule.getRulesEngine().getMetadata(element.getFqcn());
          if (metadata != null) {
            Margins insets = metadata.getInsets();
            if (insets != null) {
              // TODO:
              // Consider left or right side attachment
              // TODO: Also consider inset of element on cell to the left
              margin -= insets.top;
            }
          }

          rowHeightDp = mRule.getRulesEngine().pxToDp(margin);
        }
      }

      row++;
      mGrid.splitRow(row, insertMarginRow, rowHeightDp, mRowMatch.matchedLine);
      if (insertMarginRow) {
        row++;
      }
    }

    // Figure out where to insert the new child

    int index = mGrid.getInsertIndex(row, column);
    if (index == -1) {
      // Couldn't find a later place to insert
      newChild = targetNode.appendChild(fqcn);
    } else {
      GridModel.ViewData next = mGrid.getView(index);

      newChild = targetNode.insertChildAt(fqcn, index);

      // Must also apply positions to the following child to ensure
      // that the new child doesn't affect the implicit numbering!
      // TODO: We can later check whether the implied number is equal to
      // what it already is such that we don't need this
      next.applyPositionAttributes();
    }

    // Set the cell position (gravity) of the new widget
    int gravity = 0;
    if (mColumnMatch.type == SegmentType.RIGHT) {
      gravity |= GravityHelper.GRAVITY_RIGHT;
    } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) {
      gravity |= GravityHelper.GRAVITY_CENTER_HORIZ;
    }
    mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, column);
    if (mRowMatch.type == SegmentType.BASELINE) {
      // There *is* no baseline gravity constant, instead, leave the
      // vertical gravity unspecified and GridLayout will treat it as
      // baseline alignment
      // gravity |= GravityHelper.GRAVITY_BASELINE;
    } else if (mRowMatch.type == SegmentType.BOTTOM) {
      gravity |= GravityHelper.GRAVITY_BOTTOM;
    } else if (mRowMatch.type == SegmentType.CENTER_VERTICAL) {
      gravity |= GravityHelper.GRAVITY_CENTER_VERT;
    }
    // Ensure that we have at least one horizontal and vertical constraint, otherwise
    // the new item will be fixed. As an example, if we have a single button in the
    // table which we inserted *without* a gravity, and we then insert a button
    // above it with a vertical gravity, then only the top column would be considered
    // stretchable, and it will fill all available vertical space and the previous
    // button will jump to the bottom.
    if (!GravityHelper.isConstrainedHorizontally(gravity)) {
      gravity |= GravityHelper.GRAVITY_LEFT;
    }
    /* This causes problems: Try placing two buttons vertically from the top of the layout.
       We need to solve the free column/free row problem first.
    if (!GravityHelper.isConstrainedVertically(gravity)
            // There is no baseline constant, so we have to leave it unconstrained instead
            && mRowMatch.type != SegmentType.BASELINE
            // You also can't baseline align one element with another that has vertical
            // alignment top or bottom, so when we first "freely" place views (e.g.
            // at a particular y location), also place it freely (no constraint).
            && !mRowMatch.createCell) {
        gravity |= GravityHelper.GRAVITY_TOP;
    }
    */
    mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, getGravity(gravity));

    mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, row);

    // Apply spans to ensure that the widget can fit without pushing columns
    if (columnSpan > 1) {
      mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN_SPAN, columnSpan);
    }
    if (rowSpan > 1) {
      mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW_SPAN, rowSpan);
    }

    // Ensure that we don't store columnCount=0
    if (mGrid.actualColumnCount == 0) {
      mGrid.setGridAttribute(mGrid.layout, ATTR_COLUMN_COUNT, Math.max(1, column + 1));
    }

    return newChild;
  }