• 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.ide.common.layout.LayoutConstants.ANDROID_URI;
20 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_GRAVITY;
21 import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION;
22 import static com.android.ide.common.layout.LayoutConstants.FQCN_SPACE;
23 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL;
24 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_HORIZONTAL;
25 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_VERTICAL;
26 import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_LEFT;
27 import static com.android.ide.common.layout.LayoutConstants.VALUE_HORIZONTAL;
28 import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE;
29 
30 import com.android.ide.common.api.DrawingStyle;
31 import com.android.ide.common.api.DropFeedback;
32 import com.android.ide.common.api.IDragElement;
33 import com.android.ide.common.api.IFeedbackPainter;
34 import com.android.ide.common.api.IGraphics;
35 import com.android.ide.common.api.IMenuCallback;
36 import com.android.ide.common.api.INode;
37 import com.android.ide.common.api.INodeHandler;
38 import com.android.ide.common.api.IViewMetadata;
39 import com.android.ide.common.api.IViewMetadata.FillPreference;
40 import com.android.ide.common.api.IViewRule;
41 import com.android.ide.common.api.InsertType;
42 import com.android.ide.common.api.Point;
43 import com.android.ide.common.api.Rect;
44 import com.android.ide.common.api.RuleAction;
45 import com.android.ide.common.api.RuleAction.Choices;
46 import com.android.ide.common.api.SegmentType;
47 import com.android.ide.common.layout.grid.GridDropHandler;
48 import com.android.ide.common.layout.grid.GridLayoutPainter;
49 import com.android.ide.common.layout.grid.GridModel;
50 import com.android.util.Pair;
51 
52 import java.net.URL;
53 import java.util.Arrays;
54 import java.util.Collections;
55 import java.util.List;
56 import java.util.Map;
57 
58 /**
59  * An {@link IViewRule} for android.widget.GridLayout which provides designtime
60  * interaction with GridLayouts.
61  * <p>
62  * TODO:
63  * <ul>
64  * <li>Handle multi-drag: preserving relative positions and alignments among dragged
65  * views.
66  * <li>Handle GridLayouts that have been configured in a vertical orientation.
67  * <li>Handle free-form editing GridLayouts that have been manually edited rather than
68  * built up using free-form editing (e.g. they might not follow the same spacing
69  * convention, might use weights etc)
70  * <li>Avoid setting row and column numbers on the actual elements if they can be skipped
71  * to make the XML leaner.
72  * </ul>
73  */
74 public class GridLayoutRule extends BaseLayoutRule {
75     /**
76      * The size of the visual regular grid that we snap to (if {@link #sSnapToGrid} is set
77      */
78     public static final int GRID_SIZE = 16;
79 
80     /** Standard gap between views */
81     public static final int SHORT_GAP_DP = 16;
82 
83     /**
84      * The preferred margin size, in pixels
85      */
86     public static final int MARGIN_SIZE = 32;
87 
88     /**
89      * Size in screen pixels in the IDE of the gutter shown for new rows and columns (in
90      * grid mode)
91      */
92     private static final int NEW_CELL_WIDTH = 10;
93 
94     /**
95      * Maximum size of a widget relative to a cell which is allowed to fit into a cell
96      * (and thereby enlarge it) before it is spread with row or column spans.
97      */
98     public static final double MAX_CELL_DIFFERENCE = 1.2;
99 
100     /** Whether debugging diagnostics is available in the toolbar */
101     private static final boolean CAN_DEBUG =
102             VALUE_TRUE.equals(System.getenv("ADT_DEBUG_GRIDLAYOUT")); //$NON-NLS-1$
103 
104     private static final String ACTION_ADD_ROW = "_addrow"; //$NON-NLS-1$
105     private static final String ACTION_REMOVE_ROW = "_removerow"; //$NON-NLS-1$
106     private static final String ACTION_ADD_COL = "_addcol"; //$NON-NLS-1$
107     private static final String ACTION_REMOVE_COL = "_removecol"; //$NON-NLS-1$
108     private static final String ACTION_ORIENTATION = "_orientation"; //$NON-NLS-1$
109     private static final String ACTION_SHOW_GRID = "_grid"; //$NON-NLS-1$
110     private static final String ACTION_SNAP = "_snap"; //$NON-NLS-1$
111     private static final String ACTION_DEBUG = "_debug"; //$NON-NLS-1$
112 
113     private static final URL ICON_HORIZONTAL = GridLayoutRule.class.getResource("hlinear.png"); //$NON-NLS-1$
114     private static final URL ICON_VERTICAL = GridLayoutRule.class.getResource("vlinear.png"); //$NON-NLS-1$
115     private static final URL ICON_ADD_ROW = GridLayoutRule.class.getResource("addrow.png"); //$NON-NLS-1$
116     private static final URL ICON_REMOVE_ROW = GridLayoutRule.class.getResource("removerow.png"); //$NON-NLS-1$
117     private static final URL ICON_ADD_COL = GridLayoutRule.class.getResource("addcol.png"); //$NON-NLS-1$
118     private static final URL ICON_REMOVE_COL = GridLayoutRule.class.getResource("removecol.png"); //$NON-NLS-1$
119     private static final URL ICON_SHOW_GRID = GridLayoutRule.class.getResource("showgrid.png"); //$NON-NLS-1$
120     private static final URL ICON_SNAP = GridLayoutRule.class.getResource("snap.png"); //$NON-NLS-1$
121 
122     /**
123      * Whether the IDE should show diagnostics for debugging the grid layout - including
124      * spacers visibly in the outline, showing row and column numbers, and so on
125      */
126     public static boolean sDebugGridLayout = false;
127 
128     /** Whether the structure (grid model) should be displayed persistently to the user */
129     public static boolean sShowStructure = false;
130 
131     /** Whether the drop positions should snap to a regular grid */
132     public static boolean sSnapToGrid = false;
133 
134     /**
135      * Whether the grid is edited in "grid mode" where the operations are row/column based
136      * rather than free-form
137      */
138     public static boolean sGridMode = false;
139 
140     /** Constructs a new {@link GridLayoutRule} */
GridLayoutRule()141     public GridLayoutRule() {
142     }
143 
144     @Override
addLayoutActions(List<RuleAction> actions, final INode parentNode, final List<? extends INode> children)145     public void addLayoutActions(List<RuleAction> actions, final INode parentNode,
146             final List<? extends INode> children) {
147         super.addLayoutActions(actions, parentNode, children);
148 
149         Choices orientationAction = RuleAction.createChoices(
150                 ACTION_ORIENTATION,
151                 "Orientation", //$NON-NLS-1$
152                 new PropertyCallback(Collections.singletonList(parentNode),
153                         "Change LinearLayout Orientation", ANDROID_URI, ATTR_ORIENTATION), Arrays
154                         .<String> asList("Set Horizontal Orientation", "Set Vertical Orientation"),
155                 Arrays.<URL> asList(ICON_HORIZONTAL, ICON_VERTICAL), Arrays.<String> asList(
156                         "horizontal", "vertical"), getCurrentOrientation(parentNode),
157                 null /* icon */, -10, false);
158         orientationAction.setRadio(true);
159         actions.add(orientationAction);
160 
161         // Gravity and margins
162         if (children != null && children.size() > 0) {
163             actions.add(RuleAction.createSeparator(35));
164             actions.add(createMarginAction(parentNode, children));
165             actions.add(createGravityAction(children, ATTR_LAYOUT_GRAVITY));
166         }
167 
168         IMenuCallback actionCallback = new IMenuCallback() {
169             public void action(final RuleAction action, List<? extends INode> selectedNodes,
170                     final String valueId, final Boolean newValue) {
171                 parentNode.editXml("Add/Remove Row/Column", new INodeHandler() {
172                     public void handle(INode n) {
173                         String id = action.getId();
174                         if (id.equals(ACTION_SHOW_GRID)) {
175                             sShowStructure = !sShowStructure;
176                             // HACK: ToggleButton controls two flags for now - show grid and
177                             // grid mode (handling drags in a grid mode)
178                             sGridMode = !sGridMode;
179 
180                             mRulesEngine.redraw();
181                             return;
182                         } else if (id.equals(ACTION_SNAP)) {
183                             sSnapToGrid = !sSnapToGrid;
184                             mRulesEngine.redraw();
185                             return;
186                         } else if (id.equals(ACTION_DEBUG)) {
187                             sDebugGridLayout = !sDebugGridLayout;
188                             mRulesEngine.layout();
189                             return;
190                         }
191 
192                         GridModel grid = new GridModel(mRulesEngine, parentNode, null);
193                         if (id.equals(ACTION_ADD_ROW)) {
194                             grid.addRow(children);
195                         } else if (id.equals(ACTION_REMOVE_ROW)) {
196                             grid.removeRows(children);
197                         } else if (id.equals(ACTION_ADD_COL)) {
198                             grid.addColumn(children);
199                         } else if (id.equals(ACTION_REMOVE_COL)) {
200                             grid.removeColumns(children);
201                         }
202                     }
203 
204                 });
205             }
206         };
207 
208         // Add Row and Add Column
209         actions.add(RuleAction.createSeparator(150));
210         actions.add(RuleAction.createAction(ACTION_ADD_COL, "Add Column", actionCallback,
211                 ICON_ADD_COL, 160, false /* supportsMultipleNodes */));
212         actions.add(RuleAction.createAction(ACTION_ADD_ROW, "Add Row", actionCallback,
213                 ICON_ADD_ROW, 165, false));
214 
215         // Remove Row and Remove Column (if something is selected)
216         if (children != null && children.size() > 0) {
217             // TODO: Add "Merge Columns" and "Merge Rows" ?
218 
219             actions.add(RuleAction.createAction(ACTION_REMOVE_COL, "Remove Column",
220                     actionCallback, ICON_REMOVE_COL, 170, false));
221             actions.add(RuleAction.createAction(ACTION_REMOVE_ROW, "Remove Row",
222                     actionCallback, ICON_REMOVE_ROW, 175, false));
223         }
224 
225         actions.add(RuleAction.createSeparator(185));
226 
227         actions.add(RuleAction.createToggle(ACTION_SNAP, "Snap to Grid",
228                 sSnapToGrid, actionCallback, ICON_SNAP, 190, false));
229 
230         actions.add(RuleAction.createToggle(ACTION_SHOW_GRID, "Show Structure",
231                 sShowStructure, actionCallback, ICON_SHOW_GRID, 200, false));
232 
233         // Temporary: Diagnostics for GridLayout
234         if (CAN_DEBUG) {
235             actions.add(RuleAction.createToggle(ACTION_DEBUG, "Debug",
236                     sDebugGridLayout, actionCallback, null, 210, false));
237         }
238     }
239 
240     /**
241      * Returns the orientation attribute value currently used by the node (even if not
242      * defined, in which case the default horizontal value is returned)
243      */
getCurrentOrientation(final INode node)244     private static String getCurrentOrientation(final INode node) {
245         String orientation = node.getStringAttr(ANDROID_URI, ATTR_ORIENTATION);
246         if (orientation == null || orientation.length() == 0) {
247             orientation = VALUE_HORIZONTAL;
248         }
249         return orientation;
250     }
251 
252     @Override
onDropEnter(INode targetNode, Object targetView, IDragElement[] elements)253     public DropFeedback onDropEnter(INode targetNode, Object targetView, IDragElement[] elements) {
254         GridDropHandler userData = new GridDropHandler(this, targetNode, targetView);
255         IFeedbackPainter painter = GridLayoutPainter.createDropFeedbackPainter(this, elements);
256         return new DropFeedback(userData, painter);
257     }
258 
259     @Override
onDropMove(INode targetNode, IDragElement[] elements, DropFeedback feedback, Point p)260     public DropFeedback onDropMove(INode targetNode, IDragElement[] elements,
261             DropFeedback feedback, Point p) {
262         feedback.requestPaint = true;
263 
264         GridDropHandler handler = (GridDropHandler) feedback.userData;
265         handler.computeMatches(feedback, p);
266 
267         return feedback;
268     }
269 
270     @Override
onDropped(final INode targetNode, final IDragElement[] elements, DropFeedback feedback, Point p)271     public void onDropped(final INode targetNode, final IDragElement[] elements,
272             DropFeedback feedback, Point p) {
273         Rect b = targetNode.getBounds();
274         if (!b.isValid()) {
275             return;
276         }
277 
278         GridDropHandler dropHandler = (GridDropHandler) feedback.userData;
279         if (dropHandler.getRowMatch() == null || dropHandler.getColumnMatch() == null) {
280             return;
281         }
282 
283         // Collect IDs from dropped elements and remap them to new IDs
284         // if this is a copy or from a different canvas.
285         Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements,
286                 feedback.isCopy || !feedback.sameCanvas);
287 
288         for (IDragElement element : elements) {
289             INode newChild;
290             if (!sGridMode) {
291                 newChild = dropHandler.handleFreeFormDrop(targetNode, element);
292             } else {
293                 newChild = dropHandler.handleGridModeDrop(targetNode, element);
294             }
295 
296             // Copy all the attributes, modifying them as needed.
297             addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER);
298 
299             addInnerElements(newChild, element, idMap);
300         }
301     }
302 
303     @Override
onChildInserted(INode node, INode parent, InsertType insertType)304     public void onChildInserted(INode node, INode parent, InsertType insertType) {
305         if (insertType == InsertType.MOVE_WITHIN) {
306             // Don't adjust widths/heights/weights when just moving within a single layout
307             return;
308         }
309 
310         // Attempt to set "fill" properties on newly added views such that for example
311         // a text field will stretch horizontally.
312         String fqcn = node.getFqcn();
313         IViewMetadata metadata = mRulesEngine.getMetadata(fqcn);
314         if (metadata == null) {
315             return;
316         }
317         FillPreference fill = metadata.getFillPreference();
318         String gravity = computeDefaultGravity(fill);
319         if (gravity != null) {
320             node.setAttribute(ANDROID_URI, ATTR_LAYOUT_GRAVITY, gravity);
321         }
322     }
323 
324     /**
325      * Computes the default gravity to be used for a widget of the given fill
326      * preference when added to a grid layout
327      *
328      * @param fill the fill preference for the widget
329      * @return the gravity value, or null, to be set on the widget
330      */
computeDefaultGravity(FillPreference fill)331     public static String computeDefaultGravity(FillPreference fill) {
332         String horizontal = GRAVITY_VALUE_LEFT;
333         String vertical = null;
334         if (fill.fillHorizontally(true /*verticalContext*/)) {
335             horizontal = GRAVITY_VALUE_FILL_HORIZONTAL;
336         }
337         if (fill.fillVertically(true /*verticalContext*/)) {
338             vertical = GRAVITY_VALUE_FILL_VERTICAL;
339         }
340         String gravity;
341         if (horizontal == GRAVITY_VALUE_FILL_HORIZONTAL
342                 && vertical == GRAVITY_VALUE_FILL_VERTICAL) {
343             gravity = GRAVITY_VALUE_FILL;
344         } else if (vertical != null) {
345             gravity = horizontal + '|' + vertical;
346         } else {
347             gravity = horizontal;
348         }
349 
350         return gravity;
351     }
352 
353     @Override
onRemovingChildren(List<INode> deleted, INode parent)354     public void onRemovingChildren(List<INode> deleted, INode parent) {
355         super.onRemovingChildren(deleted, parent);
356 
357         // Attempt to clean up spacer objects for any newly-empty rows or columns
358         // as the result of this deletion
359         GridModel grid = new GridModel(mRulesEngine, parent, null);
360         for (INode child : deleted) {
361             // We don't care about deletion of spacers
362             if (child.getFqcn().equals(FQCN_SPACE)) {
363                 continue;
364             }
365             grid.markDeleted(child);
366         }
367 
368         grid.cleanup();
369     }
370 
371     @Override
paintResizeFeedback(IGraphics gc, INode node, ResizeState state)372     protected void paintResizeFeedback(IGraphics gc, INode node, ResizeState state) {
373         if (!sGridMode) {
374             GridModel grid = getGrid(state);
375             GridLayoutPainter.paintResizeFeedback(gc, state.layout, grid);
376         }
377 
378         if (resizingWidget(state)) {
379             super.paintResizeFeedback(gc, node, state);
380         } else {
381             GridModel grid = getGrid(state);
382             int startColumn = grid.getColumn(state.bounds.x);
383             int endColumn = grid.getColumn(state.bounds.x2());
384             int columnSpan = endColumn - startColumn + 1;
385 
386             int startRow = grid.getRow(state.bounds.y);
387             int endRow = grid.getRow(state.bounds.y2());
388             int rowSpan = endRow - startRow + 1;
389 
390             Rect cellBounds = grid.getCellBounds(startRow, startColumn, rowSpan, columnSpan);
391             gc.useStyle(DrawingStyle.RESIZE_PREVIEW);
392             gc.drawRect(cellBounds);
393         }
394     }
395 
396     /** Returns the grid size cached on the given {@link ResizeState} object */
getGrid(ResizeState resizeState)397     private GridModel getGrid(ResizeState resizeState) {
398         GridModel grid = (GridModel) resizeState.clientData;
399         if (grid == null) {
400             grid = new GridModel(mRulesEngine, resizeState.layout, resizeState.layoutView);
401             resizeState.clientData = grid;
402         }
403 
404         return grid;
405     }
406 
407     @Override
setNewSizeBounds(ResizeState state, INode node, INode layout, Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge)408     protected void setNewSizeBounds(ResizeState state, INode node, INode layout,
409             Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) {
410 
411         if (resizingWidget(state)) {
412             super.setNewSizeBounds(state, node, layout, oldBounds, newBounds, horizontalEdge,
413                     verticalEdge);
414         } else {
415             Pair<Integer, Integer> spans = computeResizeSpans(state);
416             int rowSpan = spans.getFirst();
417             int columnSpan = spans.getSecond();
418             GridModel.setColumnSpanAttribute(node, columnSpan);
419             GridModel.setRowSpanAttribute(node, rowSpan);
420         }
421     }
422 
423     @Override
getResizeUpdateMessage(ResizeState state, INode child, INode parent, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge)424     protected String getResizeUpdateMessage(ResizeState state, INode child, INode parent,
425             Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) {
426         Pair<Integer, Integer> spans = computeResizeSpans(state);
427         if (resizingWidget(state)) {
428             String width = state.getWidthAttribute();
429             String height = state.getHeightAttribute();
430 
431             String message;
432             if (horizontalEdge == null) {
433                 message = width;
434             } else if (verticalEdge == null) {
435                 message = height;
436             } else {
437                 // U+00D7: Unicode for multiplication sign
438                 message = String.format("%s \u00D7 %s", width, height);
439             }
440 
441             // Tack on a tip about using the Shift modifier key
442             return String.format("%s\n(Press Shift to resize row/column spans)", message);
443         } else {
444             int rowSpan = spans.getFirst();
445             int columnSpan = spans.getSecond();
446             return String.format("ColumnSpan=%d, RowSpan=%d\n(Release Shift to resize widget itself)",
447                     columnSpan, rowSpan);
448         }
449     }
450 
451     /**
452      * Returns true if we're resizing the widget, and false if we're resizing the cell
453      * spans
454      */
resizingWidget(ResizeState state)455     private static boolean resizingWidget(ResizeState state) {
456         return (state.modifierMask & DropFeedback.MODIFIER2) == 0;
457     }
458 
459     /**
460      * Computes the new column and row spans as the result of the current resizing
461      * operation
462      */
computeResizeSpans(ResizeState state)463     private Pair<Integer, Integer> computeResizeSpans(ResizeState state) {
464         GridModel grid = getGrid(state);
465 
466         int startColumn = grid.getColumn(state.bounds.x);
467         int endColumn = grid.getColumn(state.bounds.x2());
468         int columnSpan = endColumn - startColumn + 1;
469 
470         int startRow = grid.getRow(state.bounds.y);
471         int endRow = grid.getRow(state.bounds.y2());
472         int rowSpan = endRow - startRow + 1;
473 
474         return Pair.of(rowSpan, columnSpan);
475     }
476 
477     /**
478      * Returns the size of the new cell gutter in layout coordinates
479      *
480      * @return the size of the new cell gutter in layout coordinates
481      */
getNewCellSize()482     public int getNewCellSize() {
483         return mRulesEngine.screenToLayout(NEW_CELL_WIDTH / 2);
484     }
485 
486     @Override
paintSelectionFeedback(IGraphics graphics, INode parentNode, List<? extends INode> childNodes, Object view)487     public void paintSelectionFeedback(IGraphics graphics, INode parentNode,
488             List<? extends INode> childNodes, Object view) {
489         super.paintSelectionFeedback(graphics, parentNode, childNodes, view);
490 
491         if (sShowStructure) {
492             // TODO: Cache the grid
493             if (view != null) {
494                 GridLayoutPainter.paintStructure(view, DrawingStyle.GUIDELINE_DASHED,
495                         parentNode, graphics);
496             } else {
497                 GridLayoutPainter.paintStructure(DrawingStyle.GUIDELINE_DASHED,
498                         parentNode, graphics, new GridModel(mRulesEngine, parentNode, view));
499             }
500         } else if (sDebugGridLayout) {
501             GridLayoutPainter.paintStructure(DrawingStyle.GRID,
502                     parentNode, graphics, new GridModel(mRulesEngine, parentNode, view));
503         }
504 
505         // TBD: Highlight the cells around the selection, and display easy controls
506         // for for example tweaking the rowspan/colspan of a cell? (but only in grid mode)
507     }
508 }
509