• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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} */
GridLayoutRule()151     public GridLayoutRule() {
152     }
153 
154     @Override
addLayoutActions( @onNull List<RuleAction> actions, final @NonNull INode parentNode, final @NonNull List<? extends INode> children)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      */
getCurrentOrientation(final INode node)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
onDropEnter(@onNull INode targetNode, @Nullable Object targetView, @Nullable IDragElement[] elements)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
onDropMove(@onNull INode targetNode, @NonNull IDragElement[] elements, @Nullable DropFeedback feedback, @NonNull Point p)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
onDropped(final @NonNull INode targetNode, final @NonNull IDragElement[] elements, @Nullable DropFeedback feedback, @NonNull Point p)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
onChildInserted(@onNull INode node, @NonNull INode parent, @NonNull InsertType insertType)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      */
getNamespace(INode layout)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      */
computeDefaultGravity(FillPreference fill)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
onRemovingChildren(@onNull List<INode> deleted, @NonNull INode parent, boolean moved)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
paintResizeFeedback(IGraphics gc, INode node, ResizeState state)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 */
getGrid(ResizeState resizeState)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
setNewSizeBounds(ResizeState state, INode node, INode layout, Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge)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
getResizeUpdateMessage(ResizeState state, INode child, INode parent, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge)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      */
resizingWidget(ResizeState state)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      */
computeResizeSpans(ResizeState state)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      */
getNewCellSize()581     public int getNewCellSize() {
582         return mRulesEngine.screenToLayout(NEW_CELL_WIDTH / 2);
583     }
584 
585     @Override
paintSelectionFeedback(@onNull IGraphics graphics, @NonNull INode parentNode, @NonNull List<? extends INode> childNodes, @Nullable Object view)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
onPaste( @onNull INode targetNode, @Nullable Object targetView, @NonNull IDragElement[] elements)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