Home | History | Annotate | Download | only in layout
      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 
     17 package com.android.ide.common.layout;
     18 
     19 import static com.android.SdkConstants.ANDROID_URI;
     20 import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
     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_ORIENTATION;
     24 import static com.android.SdkConstants.FQCN_GRID_LAYOUT;
     25 import static com.android.SdkConstants.FQCN_SPACE;
     26 import static com.android.SdkConstants.FQCN_SPACE_V7;
     27 import static com.android.SdkConstants.GRAVITY_VALUE_FILL;
     28 import static com.android.SdkConstants.GRAVITY_VALUE_FILL_HORIZONTAL;
     29 import static com.android.SdkConstants.GRAVITY_VALUE_FILL_VERTICAL;
     30 import static com.android.SdkConstants.GRAVITY_VALUE_LEFT;
     31 import static com.android.SdkConstants.GRID_LAYOUT;
     32 import static com.android.SdkConstants.VALUE_HORIZONTAL;
     33 import static com.android.SdkConstants.VALUE_TRUE;
     34 
     35 import com.android.annotations.NonNull;
     36 import com.android.annotations.Nullable;
     37 import com.android.ide.common.api.DrawingStyle;
     38 import com.android.ide.common.api.DropFeedback;
     39 import com.android.ide.common.api.IDragElement;
     40 import com.android.ide.common.api.IFeedbackPainter;
     41 import com.android.ide.common.api.IGraphics;
     42 import com.android.ide.common.api.IMenuCallback;
     43 import com.android.ide.common.api.INode;
     44 import com.android.ide.common.api.INodeHandler;
     45 import com.android.ide.common.api.IViewMetadata;
     46 import com.android.ide.common.api.IViewMetadata.FillPreference;
     47 import com.android.ide.common.api.IViewRule;
     48 import com.android.ide.common.api.InsertType;
     49 import com.android.ide.common.api.Point;
     50 import com.android.ide.common.api.Rect;
     51 import com.android.ide.common.api.RuleAction;
     52 import com.android.ide.common.api.RuleAction.Choices;
     53 import com.android.ide.common.api.SegmentType;
     54 import com.android.ide.common.layout.grid.GridDropHandler;
     55 import com.android.ide.common.layout.grid.GridLayoutPainter;
     56 import com.android.ide.common.layout.grid.GridModel;
     57 import com.android.ide.common.layout.grid.GridModel.ViewData;
     58 import com.android.utils.Pair;
     59 
     60 import java.net.URL;
     61 import java.util.Arrays;
     62 import java.util.Collections;
     63 import java.util.List;
     64 import java.util.Map;
     65 
     66 /**
     67  * An {@link IViewRule} for android.widget.GridLayout which provides designtime
     68  * interaction with GridLayouts.
     69  * <p>
     70  * TODO:
     71  * <ul>
     72  * <li>Handle multi-drag: preserving relative positions and alignments among dragged
     73  * views.
     74  * <li>Handle GridLayouts that have been configured in a vertical orientation.
     75  * <li>Handle free-form editing GridLayouts that have been manually edited rather than
     76  * built up using free-form editing (e.g. they might not follow the same spacing
     77  * convention, might use weights etc)
     78  * <li>Avoid setting row and column numbers on the actual elements if they can be skipped
     79  * to make the XML leaner.
     80  * </ul>
     81  */
     82 public class GridLayoutRule extends BaseLayoutRule {
     83     /**
     84      * The size of the visual regular grid that we snap to (if {@link #sSnapToGrid} is set
     85      */
     86     public static final int GRID_SIZE = 16;
     87 
     88     /** Standard gap between views */
     89     public static final int SHORT_GAP_DP = 16;
     90 
     91     /**
     92      * The preferred margin size, in pixels
     93      */
     94     public static final int MARGIN_SIZE = 32;
     95 
     96     /**
     97      * Size in screen pixels in the IDE of the gutter shown for new rows and columns (in
     98      * grid mode)
     99      */
    100     private static final int NEW_CELL_WIDTH = 10;
    101 
    102     /**
    103      * Maximum size of a widget relative to a cell which is allowed to fit into a cell
    104      * (and thereby enlarge it) before it is spread with row or column spans.
    105      */
    106     public static final double MAX_CELL_DIFFERENCE = 1.2;
    107 
    108     /** Whether debugging diagnostics is available in the toolbar */
    109     private static final boolean CAN_DEBUG =
    110             VALUE_TRUE.equals(System.getenv("ADT_DEBUG_GRIDLAYOUT")); //$NON-NLS-1$
    111 
    112     private static final String ACTION_ADD_ROW = "_addrow"; //$NON-NLS-1$
    113     private static final String ACTION_REMOVE_ROW = "_removerow"; //$NON-NLS-1$
    114     private static final String ACTION_ADD_COL = "_addcol"; //$NON-NLS-1$
    115     private static final String ACTION_REMOVE_COL = "_removecol"; //$NON-NLS-1$
    116     private static final String ACTION_ORIENTATION = "_orientation"; //$NON-NLS-1$
    117     private static final String ACTION_SHOW_STRUCTURE = "_structure"; //$NON-NLS-1$
    118     private static final String ACTION_GRID_MODE = "_gridmode"; //$NON-NLS-1$
    119     private static final String ACTION_SNAP = "_snap"; //$NON-NLS-1$
    120     private static final String ACTION_DEBUG = "_debug"; //$NON-NLS-1$
    121 
    122     private static final URL ICON_HORIZONTAL = GridLayoutRule.class.getResource("hlinear.png"); //$NON-NLS-1$
    123     private static final URL ICON_VERTICAL = GridLayoutRule.class.getResource("vlinear.png"); //$NON-NLS-1$
    124     private static final URL ICON_ADD_ROW = GridLayoutRule.class.getResource("addrow.png"); //$NON-NLS-1$
    125     private static final URL ICON_REMOVE_ROW = GridLayoutRule.class.getResource("removerow.png"); //$NON-NLS-1$
    126     private static final URL ICON_ADD_COL = GridLayoutRule.class.getResource("addcol.png"); //$NON-NLS-1$
    127     private static final URL ICON_REMOVE_COL = GridLayoutRule.class.getResource("removecol.png"); //$NON-NLS-1$
    128     private static final URL ICON_SHOW_STRUCT = GridLayoutRule.class.getResource("showgrid.png"); //$NON-NLS-1$
    129     private static final URL ICON_GRID_MODE = GridLayoutRule.class.getResource("gridmode.png"); //$NON-NLS-1$
    130     private static final URL ICON_SNAP = GridLayoutRule.class.getResource("snap.png"); //$NON-NLS-1$
    131 
    132     /**
    133      * Whether the IDE should show diagnostics for debugging the grid layout - including
    134      * spacers visibly in the outline, showing row and column numbers, and so on
    135      */
    136     public static boolean sDebugGridLayout = CAN_DEBUG;
    137 
    138     /** Whether the structure (grid model) should be displayed persistently to the user */
    139     public static boolean sShowStructure = false;
    140 
    141     /** Whether the drop positions should snap to a regular grid */
    142     public static boolean sSnapToGrid = false;
    143 
    144     /**
    145      * Whether the grid is edited in "grid mode" where the operations are row/column based
    146      * rather than free-form
    147      */
    148     public static boolean sGridMode = true;
    149 
    150     /** Constructs a new {@link GridLayoutRule} */
    151     public GridLayoutRule() {
    152     }
    153 
    154     @Override
    155     public void addLayoutActions(
    156             @NonNull List<RuleAction> actions,
    157             final @NonNull INode parentNode,
    158             final @NonNull List<? extends INode> children) {
    159         super.addLayoutActions(actions, parentNode, children);
    160 
    161         String namespace = getNamespace(parentNode);
    162         Choices orientationAction = RuleAction.createChoices(
    163                 ACTION_ORIENTATION,
    164                 "Orientation", //$NON-NLS-1$
    165                 new PropertyCallback(Collections.singletonList(parentNode),
    166                         "Change LinearLayout Orientation", namespace, ATTR_ORIENTATION), Arrays
    167                         .<String> asList("Set Horizontal Orientation", "Set Vertical Orientation"),
    168                 Arrays.<URL> asList(ICON_HORIZONTAL, ICON_VERTICAL), Arrays.<String> asList(
    169                         "horizontal", "vertical"), getCurrentOrientation(parentNode),
    170                 null /* icon */, -10, false);
    171         orientationAction.setRadio(true);
    172         actions.add(orientationAction);
    173 
    174         // Gravity and margins
    175         if (children != null && children.size() > 0) {
    176             actions.add(RuleAction.createSeparator(35));
    177             actions.add(createMarginAction(parentNode, children));
    178             actions.add(createGravityAction(children, ATTR_LAYOUT_GRAVITY));
    179         }
    180 
    181         IMenuCallback actionCallback = new IMenuCallback() {
    182             @Override
    183             public void action(
    184                     final @NonNull RuleAction action,
    185                     @NonNull List<? extends INode> selectedNodes,
    186                     final @Nullable String valueId,
    187                     final @Nullable Boolean newValue) {
    188                 parentNode.editXml("Add/Remove Row/Column", new INodeHandler() {
    189                     @Override
    190                     public void handle(@NonNull INode n) {
    191                         String id = action.getId();
    192                         if (id.equals(ACTION_SHOW_STRUCTURE)) {
    193                             sShowStructure = !sShowStructure;
    194                             mRulesEngine.redraw();
    195                             return;
    196                         } else if (id.equals(ACTION_GRID_MODE)) {
    197                             sGridMode = !sGridMode;
    198                             mRulesEngine.redraw();
    199                             return;
    200                         } else if (id.equals(ACTION_SNAP)) {
    201                             sSnapToGrid = !sSnapToGrid;
    202                             mRulesEngine.redraw();
    203                             return;
    204                         } else if (id.equals(ACTION_DEBUG)) {
    205                             sDebugGridLayout = !sDebugGridLayout;
    206                             mRulesEngine.layout();
    207                             return;
    208                         }
    209 
    210                         GridModel grid = GridModel.get(mRulesEngine, parentNode, null);
    211                         if (id.equals(ACTION_ADD_ROW)) {
    212                             grid.addRow(children);
    213                         } else if (id.equals(ACTION_REMOVE_ROW)) {
    214                             grid.removeRows(children);
    215                         } else if (id.equals(ACTION_ADD_COL)) {
    216                             grid.addColumn(children);
    217                         } else if (id.equals(ACTION_REMOVE_COL)) {
    218                             grid.removeColumns(children);
    219                         }
    220                     }
    221 
    222                 });
    223             }
    224         };
    225 
    226         actions.add(RuleAction.createSeparator(142));
    227 
    228         actions.add(RuleAction.createToggle(ACTION_GRID_MODE, "Grid Model Mode",
    229                 sGridMode, actionCallback, ICON_GRID_MODE, 145, false));
    230 
    231         // Add and Remove Column actions only apply in Grid Mode
    232         if (sGridMode) {
    233             actions.add(RuleAction.createToggle(ACTION_SHOW_STRUCTURE, "Show Structure",
    234                     sShowStructure, actionCallback, ICON_SHOW_STRUCT, 147, false));
    235 
    236             // Add Row and Add Column
    237             actions.add(RuleAction.createSeparator(150));
    238             actions.add(RuleAction.createAction(ACTION_ADD_COL, "Add Column", actionCallback,
    239                     ICON_ADD_COL, 160, false /* supportsMultipleNodes */));
    240             actions.add(RuleAction.createAction(ACTION_ADD_ROW, "Add Row", actionCallback,
    241                     ICON_ADD_ROW, 165, false));
    242 
    243             // Remove Row and Remove Column (if something is selected)
    244             if (children != null && children.size() > 0) {
    245                 // TODO: Add "Merge Columns" and "Merge Rows" ?
    246 
    247                 actions.add(RuleAction.createAction(ACTION_REMOVE_COL, "Remove Column",
    248                         actionCallback, ICON_REMOVE_COL, 170, false));
    249                 actions.add(RuleAction.createAction(ACTION_REMOVE_ROW, "Remove Row",
    250                         actionCallback, ICON_REMOVE_ROW, 175, false));
    251             }
    252 
    253             actions.add(RuleAction.createSeparator(185));
    254         } else {
    255             actions.add(RuleAction.createToggle(ACTION_SHOW_STRUCTURE, "Show Structure",
    256                     sShowStructure, actionCallback, ICON_SHOW_STRUCT, 190, false));
    257 
    258             // Snap to Grid and Show Structure are only relevant in free form mode
    259             actions.add(RuleAction.createToggle(ACTION_SNAP, "Snap to Grid",
    260                     sSnapToGrid, actionCallback, ICON_SNAP, 200, false));
    261         }
    262 
    263         // Temporary: Diagnostics for GridLayout
    264         if (CAN_DEBUG) {
    265             actions.add(RuleAction.createToggle(ACTION_DEBUG, "Debug",
    266                     sDebugGridLayout, actionCallback, null, 210, false));
    267         }
    268     }
    269 
    270     /**
    271      * Returns the orientation attribute value currently used by the node (even if not
    272      * defined, in which case the default horizontal value is returned)
    273      */
    274     private String getCurrentOrientation(final INode node) {
    275         String orientation = node.getStringAttr(getNamespace(node), ATTR_ORIENTATION);
    276         if (orientation == null || orientation.length() == 0) {
    277             orientation = VALUE_HORIZONTAL;
    278         }
    279         return orientation;
    280     }
    281 
    282     @Override
    283     public DropFeedback onDropEnter(@NonNull INode targetNode, @Nullable Object targetView,
    284             @Nullable IDragElement[] elements) {
    285         GridDropHandler userData = new GridDropHandler(this, targetNode, targetView);
    286         IFeedbackPainter painter = GridLayoutPainter.createDropFeedbackPainter(this, elements);
    287         return new DropFeedback(userData, painter);
    288     }
    289 
    290     @Override
    291     public DropFeedback onDropMove(@NonNull INode targetNode, @NonNull IDragElement[] elements,
    292             @Nullable DropFeedback feedback, @NonNull Point p) {
    293         if (feedback == null) {
    294             return null;
    295         }
    296         feedback.requestPaint = true;
    297 
    298         GridDropHandler handler = (GridDropHandler) feedback.userData;
    299         handler.computeMatches(feedback, p);
    300 
    301         return feedback;
    302     }
    303 
    304     @Override
    305     public void onDropped(final @NonNull INode targetNode, final @NonNull IDragElement[] elements,
    306             @Nullable DropFeedback feedback, @NonNull Point p) {
    307         if (feedback == null) {
    308             return;
    309         }
    310 
    311         Rect b = targetNode.getBounds();
    312         if (!b.isValid()) {
    313             return;
    314         }
    315 
    316         GridDropHandler dropHandler = (GridDropHandler) feedback.userData;
    317         if (dropHandler.getRowMatch() == null || dropHandler.getColumnMatch() == null) {
    318             return;
    319         }
    320 
    321         // Collect IDs from dropped elements and remap them to new IDs
    322         // if this is a copy or from a different canvas.
    323         Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements,
    324                 feedback.isCopy || !feedback.sameCanvas);
    325 
    326         for (IDragElement element : elements) {
    327             INode newChild;
    328             if (!sGridMode) {
    329                 newChild = dropHandler.handleFreeFormDrop(targetNode, element);
    330             } else {
    331                 newChild = dropHandler.handleGridModeDrop(targetNode, element);
    332             }
    333 
    334             // Copy all the attributes, modifying them as needed.
    335             addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER);
    336 
    337             addInnerElements(newChild, element, idMap);
    338         }
    339     }
    340 
    341     @Override
    342     public void onChildInserted(@NonNull INode node, @NonNull INode parent,
    343             @NonNull InsertType insertType) {
    344         if (insertType == InsertType.MOVE_WITHIN) {
    345             // Don't adjust widths/heights/weights when just moving within a single layout
    346             return;
    347         }
    348 
    349         if (GridModel.isSpace(node.getFqcn())) {
    350             return;
    351         }
    352 
    353         // Attempt to set "fill" properties on newly added views such that for example
    354         // a text field will stretch horizontally.
    355         String fqcn = node.getFqcn();
    356         IViewMetadata metadata = mRulesEngine.getMetadata(fqcn);
    357         FillPreference fill = metadata.getFillPreference();
    358         String gravity = computeDefaultGravity(fill);
    359         if (gravity != null) {
    360             node.setAttribute(getNamespace(parent), ATTR_LAYOUT_GRAVITY, gravity);
    361         }
    362     }
    363 
    364     /**
    365      * Returns the namespace URI to use for GridLayout-specific attributes, such
    366      * as columnCount, layout_column, layout_column_span, layout_gravity etc.
    367      *
    368      * @param layout the GridLayout instance to look up the namespace for
    369      * @return the namespace, never null
    370      */
    371     public String getNamespace(INode layout) {
    372         String namespace = ANDROID_URI;
    373 
    374         String fqcn = layout.getFqcn();
    375         if (!fqcn.equals(GRID_LAYOUT) && !fqcn.equals(FQCN_GRID_LAYOUT)) {
    376             namespace = mRulesEngine.getAppNameSpace();
    377         }
    378 
    379         return namespace;
    380     }
    381 
    382     /**
    383      * Computes the default gravity to be used for a widget of the given fill
    384      * preference when added to a grid layout
    385      *
    386      * @param fill the fill preference for the widget
    387      * @return the gravity value, or null, to be set on the widget
    388      */
    389     public static String computeDefaultGravity(FillPreference fill) {
    390         String horizontal = GRAVITY_VALUE_LEFT;
    391         String vertical = null;
    392         if (fill.fillHorizontally(true /*verticalContext*/)) {
    393             horizontal = GRAVITY_VALUE_FILL_HORIZONTAL;
    394         }
    395         if (fill.fillVertically(true /*verticalContext*/)) {
    396             vertical = GRAVITY_VALUE_FILL_VERTICAL;
    397         }
    398         String gravity;
    399         if (horizontal == GRAVITY_VALUE_FILL_HORIZONTAL
    400                 && vertical == GRAVITY_VALUE_FILL_VERTICAL) {
    401             gravity = GRAVITY_VALUE_FILL;
    402         } else if (vertical != null) {
    403             gravity = horizontal + '|' + vertical;
    404         } else {
    405             gravity = horizontal;
    406         }
    407 
    408         return gravity;
    409     }
    410 
    411     @Override
    412     public void onRemovingChildren(@NonNull List<INode> deleted, @NonNull INode parent,
    413             boolean moved) {
    414         super.onRemovingChildren(deleted, parent, moved);
    415 
    416         if (!sGridMode) {
    417             // Attempt to clean up spacer objects for any newly-empty rows or columns
    418             // as the result of this deletion
    419             GridModel grid = GridModel.get(mRulesEngine, parent, null);
    420             grid.onDeleted(deleted);
    421         }
    422     }
    423 
    424     @Override
    425     protected void paintResizeFeedback(IGraphics gc, INode node, ResizeState state) {
    426         if (!sGridMode) {
    427             GridModel grid = getGrid(state);
    428             GridLayoutPainter.paintResizeFeedback(gc, state.layout, grid);
    429         }
    430 
    431         if (resizingWidget(state)) {
    432             super.paintResizeFeedback(gc, node, state);
    433         } else {
    434             GridModel grid = getGrid(state);
    435             int startColumn = grid.getColumn(state.bounds.x);
    436             int endColumn = grid.getColumn(state.bounds.x2());
    437             int columnSpan = endColumn - startColumn + 1;
    438 
    439             int startRow = grid.getRow(state.bounds.y);
    440             int endRow = grid.getRow(state.bounds.y2());
    441             int rowSpan = endRow - startRow + 1;
    442 
    443             Rect cellBounds = grid.getCellBounds(startRow, startColumn, rowSpan, columnSpan);
    444             gc.useStyle(DrawingStyle.RESIZE_PREVIEW);
    445             gc.drawRect(cellBounds);
    446         }
    447     }
    448 
    449     /** Returns the grid size cached on the given {@link ResizeState} object */
    450     private GridModel getGrid(ResizeState resizeState) {
    451         GridModel grid = (GridModel) resizeState.clientData;
    452         if (grid == null) {
    453             grid = GridModel.get(mRulesEngine, resizeState.layout, resizeState.layoutView);
    454             resizeState.clientData = grid;
    455         }
    456 
    457         return grid;
    458     }
    459 
    460     @Override
    461     protected void setNewSizeBounds(ResizeState state, INode node, INode layout,
    462             Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) {
    463 
    464         if (resizingWidget(state)) {
    465             if (state.fillWidth || state.fillHeight || state.wrapWidth || state.wrapHeight) {
    466                 GridModel grid = getGrid(state);
    467                 ViewData view = grid.getView(node);
    468                 if (view != null) {
    469                     String gravityString = grid.getGridAttribute(view.node, ATTR_LAYOUT_GRAVITY);
    470                     int gravity = GravityHelper.getGravity(gravityString, 0);
    471                     if (view.column > 0 && verticalEdge != null && state.fillWidth) {
    472                         state.fillWidth = false;
    473                         state.wrapWidth = true;
    474                         gravity &= ~GravityHelper.GRAVITY_HORIZ_MASK;
    475                         gravity |= GravityHelper.GRAVITY_FILL_HORIZ;
    476                     } else if (verticalEdge != null && state.wrapWidth) {
    477                         gravity &= ~GravityHelper.GRAVITY_HORIZ_MASK;
    478                         gravity |= GravityHelper.GRAVITY_LEFT;
    479                     }
    480                     if (view.row > 0 && horizontalEdge != null && state.fillHeight) {
    481                         state.fillHeight = false;
    482                         state.wrapHeight = true;
    483                         gravity &= ~GravityHelper.GRAVITY_VERT_MASK;
    484                         gravity |= GravityHelper.GRAVITY_FILL_VERT;
    485                     } else if (horizontalEdge != null && state.wrapHeight) {
    486                         gravity &= ~GravityHelper.GRAVITY_VERT_MASK;
    487                         gravity |= GravityHelper.GRAVITY_TOP;
    488                     }
    489                     gravityString = GravityHelper.getGravity(gravity);
    490                     grid.setGridAttribute(view.node, ATTR_LAYOUT_GRAVITY, gravityString);
    491                     // Fall through and set layout_width and/or layout_height to wrap_content
    492                 }
    493             }
    494             super.setNewSizeBounds(state, node, layout, oldBounds, newBounds, horizontalEdge,
    495                     verticalEdge);
    496         } else {
    497             Pair<Integer, Integer> spans = computeResizeSpans(state);
    498             int rowSpan = spans.getFirst();
    499             int columnSpan = spans.getSecond();
    500             GridModel grid = getGrid(state);
    501             grid.setColumnSpanAttribute(node, columnSpan);
    502             grid.setRowSpanAttribute(node, rowSpan);
    503 
    504             ViewData view = grid.getView(node);
    505             if (view != null) {
    506                 String gravityString = grid.getGridAttribute(view.node, ATTR_LAYOUT_GRAVITY);
    507                 int gravity = GravityHelper.getGravity(gravityString, 0);
    508                 if (verticalEdge != null && columnSpan > 1) {
    509                     gravity &= ~GravityHelper.GRAVITY_HORIZ_MASK;
    510                     gravity |= GravityHelper.GRAVITY_FILL_HORIZ;
    511                 }
    512                 if (horizontalEdge != null && rowSpan > 1) {
    513                     gravity &= ~GravityHelper.GRAVITY_VERT_MASK;
    514                     gravity |= GravityHelper.GRAVITY_FILL_VERT;
    515                 }
    516                 gravityString = GravityHelper.getGravity(gravity);
    517                 grid.setGridAttribute(view.node, ATTR_LAYOUT_GRAVITY, gravityString);
    518             }
    519         }
    520     }
    521 
    522     @Override
    523     protected String getResizeUpdateMessage(ResizeState state, INode child, INode parent,
    524             Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) {
    525         Pair<Integer, Integer> spans = computeResizeSpans(state);
    526         if (resizingWidget(state)) {
    527             String width = state.getWidthAttribute();
    528             String height = state.getHeightAttribute();
    529 
    530             String message;
    531             if (horizontalEdge == null) {
    532                 message = width;
    533             } else if (verticalEdge == null) {
    534                 message = height;
    535             } else {
    536                 // U+00D7: Unicode for multiplication sign
    537                 message = String.format("%s \u00D7 %s", width, height);
    538             }
    539 
    540             // Tack on a tip about using the Shift modifier key
    541             return String.format("%s\n(Press Shift to resize row/column spans)", message);
    542         } else {
    543             int rowSpan = spans.getFirst();
    544             int columnSpan = spans.getSecond();
    545             return String.format("ColumnSpan=%d, RowSpan=%d\n(Release Shift to resize widget itself)",
    546                     columnSpan, rowSpan);
    547         }
    548     }
    549 
    550     /**
    551      * Returns true if we're resizing the widget, and false if we're resizing the cell
    552      * spans
    553      */
    554     private static boolean resizingWidget(ResizeState state) {
    555         return (state.modifierMask & DropFeedback.MODIFIER2) == 0;
    556     }
    557 
    558     /**
    559      * Computes the new column and row spans as the result of the current resizing
    560      * operation
    561      */
    562     private Pair<Integer, Integer> computeResizeSpans(ResizeState state) {
    563         GridModel grid = getGrid(state);
    564 
    565         int startColumn = grid.getColumn(state.bounds.x);
    566         int endColumn = grid.getColumn(state.bounds.x2());
    567         int columnSpan = endColumn - startColumn + 1;
    568 
    569         int startRow = grid.getRow(state.bounds.y);
    570         int endRow = grid.getRow(state.bounds.y2());
    571         int rowSpan = endRow - startRow + 1;
    572 
    573         return Pair.of(rowSpan, columnSpan);
    574     }
    575 
    576     /**
    577      * Returns the size of the new cell gutter in layout coordinates
    578      *
    579      * @return the size of the new cell gutter in layout coordinates
    580      */
    581     public int getNewCellSize() {
    582         return mRulesEngine.screenToLayout(NEW_CELL_WIDTH / 2);
    583     }
    584 
    585     @Override
    586     public void paintSelectionFeedback(@NonNull IGraphics graphics, @NonNull INode parentNode,
    587             @NonNull List<? extends INode> childNodes, @Nullable Object view) {
    588         super.paintSelectionFeedback(graphics, parentNode, childNodes, view);
    589 
    590         if (sShowStructure) {
    591             // TODO: Cache the grid
    592             if (view != null) {
    593                 if (GridLayoutPainter.paintStructure(view, DrawingStyle.GUIDELINE_DASHED,
    594                         parentNode, graphics)) {
    595                     return;
    596                 }
    597             }
    598             GridLayoutPainter.paintStructure(DrawingStyle.GUIDELINE_DASHED,
    599                         parentNode, graphics, GridModel.get(mRulesEngine, parentNode, view));
    600         } else if (sDebugGridLayout) {
    601             GridLayoutPainter.paintStructure(DrawingStyle.GRID,
    602                     parentNode, graphics, GridModel.get(mRulesEngine, parentNode, view));
    603         }
    604 
    605         // TBD: Highlight the cells around the selection, and display easy controls
    606         // for for example tweaking the rowspan/colspan of a cell? (but only in grid mode)
    607     }
    608 
    609     /**
    610      * Paste into a GridLayout. We have several possible behaviors (and many
    611      * more than are listed here):
    612      * <ol>
    613      * <li> Preserve the current positions of the elements (if pasted from another
    614      *      canvas, not just XML markup copied from say a web site) and apply those
    615      *      into the current grid. This might mean "overwriting" (sitting on top of)
    616      *      existing elements.
    617      * <li> Fill available "holes" in the grid.
    618      * <li> Lay them out consecutively, row by row, like text.
    619      * <li> Some hybrid approach, where I attempt to preserve the <b>relative</b>
    620      *      relationships (columns/wrapping, spacing between the pasted views etc)
    621      *      but I append them to the bottom of the layout on one or more new rows.
    622      * <li> Try to paste at the current mouse position, if known, preserving the
    623      *      relative distances between the existing elements there.
    624      * </ol>
    625      * Attempting to preserve the current position isn't possible right now,
    626      * because the clipboard data contains only the textual representation of
    627      * the markup. (We'd need to stash position information from a previous
    628      * layout render along with the clipboard data).
    629      * <p>
    630      * Currently, this implementation simply lays out the elements row by row,
    631      * approach #3 above.
    632      */
    633     @Override
    634     public void onPaste(
    635             @NonNull INode targetNode,
    636             @Nullable Object targetView,
    637             @NonNull IDragElement[] elements) {
    638         DropFeedback feedback = onDropEnter(targetNode, targetView, elements);
    639         if (feedback != null) {
    640             Rect b = targetNode.getBounds();
    641             if (!b.isValid()) {
    642                 return;
    643             }
    644 
    645             Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements,
    646                     true /* remap id's */);
    647 
    648             for (IDragElement element : elements) {
    649                 // Skip <Space> elements and only insert the real elements being
    650                 // copied
    651                 if (elements.length > 1 && (FQCN_SPACE.equals(element.getFqcn())
    652                         || FQCN_SPACE_V7.equals(element.getFqcn()))) {
    653                     continue;
    654                 }
    655 
    656                 String fqcn = element.getFqcn();
    657                 INode newChild = targetNode.appendChild(fqcn);
    658                 addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER);
    659 
    660                 // Ensure that we reset any potential row/column attributes from a different
    661                 // grid layout being copied from
    662                 GridDropHandler handler = (GridDropHandler) feedback.userData;
    663                 GridModel grid = handler.getGrid();
    664                 grid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, null);
    665                 grid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, null);
    666 
    667                 // TODO: Set columnSpans to avoid making these widgets completely
    668                 // break the layout
    669                 // Alternatively, I could just lay them all out on subsequent lines
    670                 // with a column span of columnSpan5
    671 
    672                 addInnerElements(newChild, element, idMap);
    673             }
    674         }
    675     }
    676 }
    677