| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Eclipse Public License, Version 1.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.eclipse.org/org/documents/epl-v10.php |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.ide.common.layout; |
| |
| import static com.android.SdkConstants.ANDROID_URI; |
| import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN; |
| import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY; |
| import static com.android.SdkConstants.ATTR_LAYOUT_ROW; |
| import static com.android.SdkConstants.ATTR_ORIENTATION; |
| import static com.android.SdkConstants.FQCN_GRID_LAYOUT; |
| import static com.android.SdkConstants.FQCN_SPACE; |
| import static com.android.SdkConstants.FQCN_SPACE_V7; |
| import static com.android.SdkConstants.GRAVITY_VALUE_FILL; |
| import static com.android.SdkConstants.GRAVITY_VALUE_FILL_HORIZONTAL; |
| import static com.android.SdkConstants.GRAVITY_VALUE_FILL_VERTICAL; |
| import static com.android.SdkConstants.GRAVITY_VALUE_LEFT; |
| import static com.android.SdkConstants.GRID_LAYOUT; |
| import static com.android.SdkConstants.VALUE_HORIZONTAL; |
| import static com.android.SdkConstants.VALUE_TRUE; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.ide.common.api.DrawingStyle; |
| import com.android.ide.common.api.DropFeedback; |
| import com.android.ide.common.api.IDragElement; |
| import com.android.ide.common.api.IFeedbackPainter; |
| import com.android.ide.common.api.IGraphics; |
| import com.android.ide.common.api.IMenuCallback; |
| import com.android.ide.common.api.INode; |
| import com.android.ide.common.api.INodeHandler; |
| import com.android.ide.common.api.IViewMetadata; |
| import com.android.ide.common.api.IViewMetadata.FillPreference; |
| import com.android.ide.common.api.IViewRule; |
| import com.android.ide.common.api.InsertType; |
| import com.android.ide.common.api.Point; |
| import com.android.ide.common.api.Rect; |
| import com.android.ide.common.api.RuleAction; |
| import com.android.ide.common.api.RuleAction.Choices; |
| import com.android.ide.common.api.SegmentType; |
| import com.android.ide.common.layout.grid.GridDropHandler; |
| import com.android.ide.common.layout.grid.GridLayoutPainter; |
| import com.android.ide.common.layout.grid.GridModel; |
| import com.android.ide.common.layout.grid.GridModel.ViewData; |
| import com.android.utils.Pair; |
| |
| import java.net.URL; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * An {@link IViewRule} for android.widget.GridLayout which provides designtime |
| * interaction with GridLayouts. |
| * <p> |
| * TODO: |
| * <ul> |
| * <li>Handle multi-drag: preserving relative positions and alignments among dragged |
| * views. |
| * <li>Handle GridLayouts that have been configured in a vertical orientation. |
| * <li>Handle free-form editing GridLayouts that have been manually edited rather than |
| * built up using free-form editing (e.g. they might not follow the same spacing |
| * convention, might use weights etc) |
| * <li>Avoid setting row and column numbers on the actual elements if they can be skipped |
| * to make the XML leaner. |
| * </ul> |
| */ |
| public class GridLayoutRule extends BaseLayoutRule { |
| /** |
| * The size of the visual regular grid that we snap to (if {@link #sSnapToGrid} is set |
| */ |
| public static final int GRID_SIZE = 16; |
| |
| /** Standard gap between views */ |
| public static final int SHORT_GAP_DP = 16; |
| |
| /** |
| * The preferred margin size, in pixels |
| */ |
| public static final int MARGIN_SIZE = 32; |
| |
| /** |
| * Size in screen pixels in the IDE of the gutter shown for new rows and columns (in |
| * grid mode) |
| */ |
| private static final int NEW_CELL_WIDTH = 10; |
| |
| /** |
| * Maximum size of a widget relative to a cell which is allowed to fit into a cell |
| * (and thereby enlarge it) before it is spread with row or column spans. |
| */ |
| public static final double MAX_CELL_DIFFERENCE = 1.2; |
| |
| /** Whether debugging diagnostics is available in the toolbar */ |
| private static final boolean CAN_DEBUG = |
| VALUE_TRUE.equals(System.getenv("ADT_DEBUG_GRIDLAYOUT")); //$NON-NLS-1$ |
| |
| private static final String ACTION_ADD_ROW = "_addrow"; //$NON-NLS-1$ |
| private static final String ACTION_REMOVE_ROW = "_removerow"; //$NON-NLS-1$ |
| private static final String ACTION_ADD_COL = "_addcol"; //$NON-NLS-1$ |
| private static final String ACTION_REMOVE_COL = "_removecol"; //$NON-NLS-1$ |
| private static final String ACTION_ORIENTATION = "_orientation"; //$NON-NLS-1$ |
| private static final String ACTION_SHOW_STRUCTURE = "_structure"; //$NON-NLS-1$ |
| private static final String ACTION_GRID_MODE = "_gridmode"; //$NON-NLS-1$ |
| private static final String ACTION_SNAP = "_snap"; //$NON-NLS-1$ |
| private static final String ACTION_DEBUG = "_debug"; //$NON-NLS-1$ |
| |
| private static final URL ICON_HORIZONTAL = GridLayoutRule.class.getResource("hlinear.png"); //$NON-NLS-1$ |
| private static final URL ICON_VERTICAL = GridLayoutRule.class.getResource("vlinear.png"); //$NON-NLS-1$ |
| private static final URL ICON_ADD_ROW = GridLayoutRule.class.getResource("addrow.png"); //$NON-NLS-1$ |
| private static final URL ICON_REMOVE_ROW = GridLayoutRule.class.getResource("removerow.png"); //$NON-NLS-1$ |
| private static final URL ICON_ADD_COL = GridLayoutRule.class.getResource("addcol.png"); //$NON-NLS-1$ |
| private static final URL ICON_REMOVE_COL = GridLayoutRule.class.getResource("removecol.png"); //$NON-NLS-1$ |
| private static final URL ICON_SHOW_STRUCT = GridLayoutRule.class.getResource("showgrid.png"); //$NON-NLS-1$ |
| private static final URL ICON_GRID_MODE = GridLayoutRule.class.getResource("gridmode.png"); //$NON-NLS-1$ |
| private static final URL ICON_SNAP = GridLayoutRule.class.getResource("snap.png"); //$NON-NLS-1$ |
| |
| /** |
| * Whether the IDE should show diagnostics for debugging the grid layout - including |
| * spacers visibly in the outline, showing row and column numbers, and so on |
| */ |
| public static boolean sDebugGridLayout = CAN_DEBUG; |
| |
| /** Whether the structure (grid model) should be displayed persistently to the user */ |
| public static boolean sShowStructure = false; |
| |
| /** Whether the drop positions should snap to a regular grid */ |
| public static boolean sSnapToGrid = false; |
| |
| /** |
| * Whether the grid is edited in "grid mode" where the operations are row/column based |
| * rather than free-form |
| */ |
| public static boolean sGridMode = true; |
| |
| /** Constructs a new {@link GridLayoutRule} */ |
| public GridLayoutRule() { |
| } |
| |
| @Override |
| public void addLayoutActions( |
| @NonNull List<RuleAction> actions, |
| final @NonNull INode parentNode, |
| final @NonNull List<? extends INode> children) { |
| super.addLayoutActions(actions, parentNode, children); |
| |
| String namespace = getNamespace(parentNode); |
| Choices orientationAction = RuleAction.createChoices( |
| ACTION_ORIENTATION, |
| "Orientation", //$NON-NLS-1$ |
| new PropertyCallback(Collections.singletonList(parentNode), |
| "Change LinearLayout Orientation", namespace, ATTR_ORIENTATION), Arrays |
| .<String> asList("Set Horizontal Orientation", "Set Vertical Orientation"), |
| Arrays.<URL> asList(ICON_HORIZONTAL, ICON_VERTICAL), Arrays.<String> asList( |
| "horizontal", "vertical"), getCurrentOrientation(parentNode), |
| null /* icon */, -10, false); |
| orientationAction.setRadio(true); |
| actions.add(orientationAction); |
| |
| // Gravity and margins |
| if (children != null && children.size() > 0) { |
| actions.add(RuleAction.createSeparator(35)); |
| actions.add(createMarginAction(parentNode, children)); |
| actions.add(createGravityAction(children, ATTR_LAYOUT_GRAVITY)); |
| } |
| |
| IMenuCallback actionCallback = new IMenuCallback() { |
| @Override |
| public void action( |
| final @NonNull RuleAction action, |
| @NonNull List<? extends INode> selectedNodes, |
| final @Nullable String valueId, |
| final @Nullable Boolean newValue) { |
| parentNode.editXml("Add/Remove Row/Column", new INodeHandler() { |
| @Override |
| public void handle(@NonNull INode n) { |
| String id = action.getId(); |
| if (id.equals(ACTION_SHOW_STRUCTURE)) { |
| sShowStructure = !sShowStructure; |
| mRulesEngine.redraw(); |
| return; |
| } else if (id.equals(ACTION_GRID_MODE)) { |
| sGridMode = !sGridMode; |
| mRulesEngine.redraw(); |
| return; |
| } else if (id.equals(ACTION_SNAP)) { |
| sSnapToGrid = !sSnapToGrid; |
| mRulesEngine.redraw(); |
| return; |
| } else if (id.equals(ACTION_DEBUG)) { |
| sDebugGridLayout = !sDebugGridLayout; |
| mRulesEngine.layout(); |
| return; |
| } |
| |
| GridModel grid = GridModel.get(mRulesEngine, parentNode, null); |
| if (id.equals(ACTION_ADD_ROW)) { |
| grid.addRow(children); |
| } else if (id.equals(ACTION_REMOVE_ROW)) { |
| grid.removeRows(children); |
| } else if (id.equals(ACTION_ADD_COL)) { |
| grid.addColumn(children); |
| } else if (id.equals(ACTION_REMOVE_COL)) { |
| grid.removeColumns(children); |
| } |
| } |
| |
| }); |
| } |
| }; |
| |
| actions.add(RuleAction.createSeparator(142)); |
| |
| actions.add(RuleAction.createToggle(ACTION_GRID_MODE, "Grid Model Mode", |
| sGridMode, actionCallback, ICON_GRID_MODE, 145, false)); |
| |
| // Add and Remove Column actions only apply in Grid Mode |
| if (sGridMode) { |
| actions.add(RuleAction.createToggle(ACTION_SHOW_STRUCTURE, "Show Structure", |
| sShowStructure, actionCallback, ICON_SHOW_STRUCT, 147, false)); |
| |
| // Add Row and Add Column |
| actions.add(RuleAction.createSeparator(150)); |
| actions.add(RuleAction.createAction(ACTION_ADD_COL, "Add Column", actionCallback, |
| ICON_ADD_COL, 160, false /* supportsMultipleNodes */)); |
| actions.add(RuleAction.createAction(ACTION_ADD_ROW, "Add Row", actionCallback, |
| ICON_ADD_ROW, 165, false)); |
| |
| // Remove Row and Remove Column (if something is selected) |
| if (children != null && children.size() > 0) { |
| // TODO: Add "Merge Columns" and "Merge Rows" ? |
| |
| actions.add(RuleAction.createAction(ACTION_REMOVE_COL, "Remove Column", |
| actionCallback, ICON_REMOVE_COL, 170, false)); |
| actions.add(RuleAction.createAction(ACTION_REMOVE_ROW, "Remove Row", |
| actionCallback, ICON_REMOVE_ROW, 175, false)); |
| } |
| |
| actions.add(RuleAction.createSeparator(185)); |
| } else { |
| actions.add(RuleAction.createToggle(ACTION_SHOW_STRUCTURE, "Show Structure", |
| sShowStructure, actionCallback, ICON_SHOW_STRUCT, 190, false)); |
| |
| // Snap to Grid and Show Structure are only relevant in free form mode |
| actions.add(RuleAction.createToggle(ACTION_SNAP, "Snap to Grid", |
| sSnapToGrid, actionCallback, ICON_SNAP, 200, false)); |
| } |
| |
| // Temporary: Diagnostics for GridLayout |
| if (CAN_DEBUG) { |
| actions.add(RuleAction.createToggle(ACTION_DEBUG, "Debug", |
| sDebugGridLayout, actionCallback, null, 210, false)); |
| } |
| } |
| |
| /** |
| * Returns the orientation attribute value currently used by the node (even if not |
| * defined, in which case the default horizontal value is returned) |
| */ |
| private String getCurrentOrientation(final INode node) { |
| String orientation = node.getStringAttr(getNamespace(node), ATTR_ORIENTATION); |
| if (orientation == null || orientation.length() == 0) { |
| orientation = VALUE_HORIZONTAL; |
| } |
| return orientation; |
| } |
| |
| @Override |
| public DropFeedback onDropEnter(@NonNull INode targetNode, @Nullable Object targetView, |
| @Nullable IDragElement[] elements) { |
| GridDropHandler userData = new GridDropHandler(this, targetNode, targetView); |
| IFeedbackPainter painter = GridLayoutPainter.createDropFeedbackPainter(this, elements); |
| return new DropFeedback(userData, painter); |
| } |
| |
| @Override |
| public DropFeedback onDropMove(@NonNull INode targetNode, @NonNull IDragElement[] elements, |
| @Nullable DropFeedback feedback, @NonNull Point p) { |
| if (feedback == null) { |
| return null; |
| } |
| feedback.requestPaint = true; |
| |
| GridDropHandler handler = (GridDropHandler) feedback.userData; |
| handler.computeMatches(feedback, p); |
| |
| return feedback; |
| } |
| |
| @Override |
| public void onDropped(final @NonNull INode targetNode, final @NonNull IDragElement[] elements, |
| @Nullable DropFeedback feedback, @NonNull Point p) { |
| if (feedback == null) { |
| return; |
| } |
| |
| Rect b = targetNode.getBounds(); |
| if (!b.isValid()) { |
| return; |
| } |
| |
| GridDropHandler dropHandler = (GridDropHandler) feedback.userData; |
| if (dropHandler.getRowMatch() == null || dropHandler.getColumnMatch() == null) { |
| return; |
| } |
| |
| // Collect IDs from dropped elements and remap them to new IDs |
| // if this is a copy or from a different canvas. |
| Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements, |
| feedback.isCopy || !feedback.sameCanvas); |
| |
| for (IDragElement element : elements) { |
| INode newChild; |
| if (!sGridMode) { |
| newChild = dropHandler.handleFreeFormDrop(targetNode, element); |
| } else { |
| newChild = dropHandler.handleGridModeDrop(targetNode, element); |
| } |
| |
| // Copy all the attributes, modifying them as needed. |
| addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER); |
| |
| addInnerElements(newChild, element, idMap); |
| } |
| } |
| |
| @Override |
| public void onChildInserted(@NonNull INode node, @NonNull INode parent, |
| @NonNull InsertType insertType) { |
| if (insertType == InsertType.MOVE_WITHIN) { |
| // Don't adjust widths/heights/weights when just moving within a single layout |
| return; |
| } |
| |
| if (GridModel.isSpace(node.getFqcn())) { |
| return; |
| } |
| |
| // Attempt to set "fill" properties on newly added views such that for example |
| // a text field will stretch horizontally. |
| String fqcn = node.getFqcn(); |
| IViewMetadata metadata = mRulesEngine.getMetadata(fqcn); |
| FillPreference fill = metadata.getFillPreference(); |
| String gravity = computeDefaultGravity(fill); |
| if (gravity != null) { |
| node.setAttribute(getNamespace(parent), ATTR_LAYOUT_GRAVITY, gravity); |
| } |
| } |
| |
| /** |
| * Returns the namespace URI to use for GridLayout-specific attributes, such |
| * as columnCount, layout_column, layout_column_span, layout_gravity etc. |
| * |
| * @param layout the GridLayout instance to look up the namespace for |
| * @return the namespace, never null |
| */ |
| public String getNamespace(INode layout) { |
| String namespace = ANDROID_URI; |
| |
| String fqcn = layout.getFqcn(); |
| if (!fqcn.equals(GRID_LAYOUT) && !fqcn.equals(FQCN_GRID_LAYOUT)) { |
| namespace = mRulesEngine.getAppNameSpace(); |
| } |
| |
| return namespace; |
| } |
| |
| /** |
| * Computes the default gravity to be used for a widget of the given fill |
| * preference when added to a grid layout |
| * |
| * @param fill the fill preference for the widget |
| * @return the gravity value, or null, to be set on the widget |
| */ |
| public static String computeDefaultGravity(FillPreference fill) { |
| String horizontal = GRAVITY_VALUE_LEFT; |
| String vertical = null; |
| if (fill.fillHorizontally(true /*verticalContext*/)) { |
| horizontal = GRAVITY_VALUE_FILL_HORIZONTAL; |
| } |
| if (fill.fillVertically(true /*verticalContext*/)) { |
| vertical = GRAVITY_VALUE_FILL_VERTICAL; |
| } |
| String gravity; |
| if (horizontal == GRAVITY_VALUE_FILL_HORIZONTAL |
| && vertical == GRAVITY_VALUE_FILL_VERTICAL) { |
| gravity = GRAVITY_VALUE_FILL; |
| } else if (vertical != null) { |
| gravity = horizontal + '|' + vertical; |
| } else { |
| gravity = horizontal; |
| } |
| |
| return gravity; |
| } |
| |
| @Override |
| public void onRemovingChildren(@NonNull List<INode> deleted, @NonNull INode parent, |
| boolean moved) { |
| super.onRemovingChildren(deleted, parent, moved); |
| |
| if (!sGridMode) { |
| // Attempt to clean up spacer objects for any newly-empty rows or columns |
| // as the result of this deletion |
| GridModel grid = GridModel.get(mRulesEngine, parent, null); |
| grid.onDeleted(deleted); |
| } |
| } |
| |
| @Override |
| protected void paintResizeFeedback(IGraphics gc, INode node, ResizeState state) { |
| if (!sGridMode) { |
| GridModel grid = getGrid(state); |
| GridLayoutPainter.paintResizeFeedback(gc, state.layout, grid); |
| } |
| |
| if (resizingWidget(state)) { |
| super.paintResizeFeedback(gc, node, state); |
| } else { |
| GridModel grid = getGrid(state); |
| int startColumn = grid.getColumn(state.bounds.x); |
| int endColumn = grid.getColumn(state.bounds.x2()); |
| int columnSpan = endColumn - startColumn + 1; |
| |
| int startRow = grid.getRow(state.bounds.y); |
| int endRow = grid.getRow(state.bounds.y2()); |
| int rowSpan = endRow - startRow + 1; |
| |
| Rect cellBounds = grid.getCellBounds(startRow, startColumn, rowSpan, columnSpan); |
| gc.useStyle(DrawingStyle.RESIZE_PREVIEW); |
| gc.drawRect(cellBounds); |
| } |
| } |
| |
| /** Returns the grid size cached on the given {@link ResizeState} object */ |
| private GridModel getGrid(ResizeState resizeState) { |
| GridModel grid = (GridModel) resizeState.clientData; |
| if (grid == null) { |
| grid = GridModel.get(mRulesEngine, resizeState.layout, resizeState.layoutView); |
| resizeState.clientData = grid; |
| } |
| |
| return grid; |
| } |
| |
| @Override |
| protected void setNewSizeBounds(ResizeState state, INode node, INode layout, |
| Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { |
| |
| if (resizingWidget(state)) { |
| if (state.fillWidth || state.fillHeight || state.wrapWidth || state.wrapHeight) { |
| GridModel grid = getGrid(state); |
| ViewData view = grid.getView(node); |
| if (view != null) { |
| String gravityString = grid.getGridAttribute(view.node, ATTR_LAYOUT_GRAVITY); |
| int gravity = GravityHelper.getGravity(gravityString, 0); |
| if (view.column > 0 && verticalEdge != null && state.fillWidth) { |
| state.fillWidth = false; |
| state.wrapWidth = true; |
| gravity &= ~GravityHelper.GRAVITY_HORIZ_MASK; |
| gravity |= GravityHelper.GRAVITY_FILL_HORIZ; |
| } else if (verticalEdge != null && state.wrapWidth) { |
| gravity &= ~GravityHelper.GRAVITY_HORIZ_MASK; |
| gravity |= GravityHelper.GRAVITY_LEFT; |
| } |
| if (view.row > 0 && horizontalEdge != null && state.fillHeight) { |
| state.fillHeight = false; |
| state.wrapHeight = true; |
| gravity &= ~GravityHelper.GRAVITY_VERT_MASK; |
| gravity |= GravityHelper.GRAVITY_FILL_VERT; |
| } else if (horizontalEdge != null && state.wrapHeight) { |
| gravity &= ~GravityHelper.GRAVITY_VERT_MASK; |
| gravity |= GravityHelper.GRAVITY_TOP; |
| } |
| gravityString = GravityHelper.getGravity(gravity); |
| grid.setGridAttribute(view.node, ATTR_LAYOUT_GRAVITY, gravityString); |
| // Fall through and set layout_width and/or layout_height to wrap_content |
| } |
| } |
| super.setNewSizeBounds(state, node, layout, oldBounds, newBounds, horizontalEdge, |
| verticalEdge); |
| } else { |
| Pair<Integer, Integer> spans = computeResizeSpans(state); |
| int rowSpan = spans.getFirst(); |
| int columnSpan = spans.getSecond(); |
| GridModel grid = getGrid(state); |
| grid.setColumnSpanAttribute(node, columnSpan); |
| grid.setRowSpanAttribute(node, rowSpan); |
| |
| ViewData view = grid.getView(node); |
| if (view != null) { |
| String gravityString = grid.getGridAttribute(view.node, ATTR_LAYOUT_GRAVITY); |
| int gravity = GravityHelper.getGravity(gravityString, 0); |
| if (verticalEdge != null && columnSpan > 1) { |
| gravity &= ~GravityHelper.GRAVITY_HORIZ_MASK; |
| gravity |= GravityHelper.GRAVITY_FILL_HORIZ; |
| } |
| if (horizontalEdge != null && rowSpan > 1) { |
| gravity &= ~GravityHelper.GRAVITY_VERT_MASK; |
| gravity |= GravityHelper.GRAVITY_FILL_VERT; |
| } |
| gravityString = GravityHelper.getGravity(gravity); |
| grid.setGridAttribute(view.node, ATTR_LAYOUT_GRAVITY, gravityString); |
| } |
| } |
| } |
| |
| @Override |
| protected String getResizeUpdateMessage(ResizeState state, INode child, INode parent, |
| Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { |
| Pair<Integer, Integer> spans = computeResizeSpans(state); |
| if (resizingWidget(state)) { |
| String width = state.getWidthAttribute(); |
| String height = state.getHeightAttribute(); |
| |
| String message; |
| if (horizontalEdge == null) { |
| message = width; |
| } else if (verticalEdge == null) { |
| message = height; |
| } else { |
| // U+00D7: Unicode for multiplication sign |
| message = String.format("%s \u00D7 %s", width, height); |
| } |
| |
| // Tack on a tip about using the Shift modifier key |
| return String.format("%s\n(Press Shift to resize row/column spans)", message); |
| } else { |
| int rowSpan = spans.getFirst(); |
| int columnSpan = spans.getSecond(); |
| return String.format("ColumnSpan=%d, RowSpan=%d\n(Release Shift to resize widget itself)", |
| columnSpan, rowSpan); |
| } |
| } |
| |
| /** |
| * Returns true if we're resizing the widget, and false if we're resizing the cell |
| * spans |
| */ |
| private static boolean resizingWidget(ResizeState state) { |
| return (state.modifierMask & DropFeedback.MODIFIER2) == 0; |
| } |
| |
| /** |
| * Computes the new column and row spans as the result of the current resizing |
| * operation |
| */ |
| private Pair<Integer, Integer> computeResizeSpans(ResizeState state) { |
| GridModel grid = getGrid(state); |
| |
| int startColumn = grid.getColumn(state.bounds.x); |
| int endColumn = grid.getColumn(state.bounds.x2()); |
| int columnSpan = endColumn - startColumn + 1; |
| |
| int startRow = grid.getRow(state.bounds.y); |
| int endRow = grid.getRow(state.bounds.y2()); |
| int rowSpan = endRow - startRow + 1; |
| |
| return Pair.of(rowSpan, columnSpan); |
| } |
| |
| /** |
| * Returns the size of the new cell gutter in layout coordinates |
| * |
| * @return the size of the new cell gutter in layout coordinates |
| */ |
| public int getNewCellSize() { |
| return mRulesEngine.screenToLayout(NEW_CELL_WIDTH / 2); |
| } |
| |
| @Override |
| public void paintSelectionFeedback(@NonNull IGraphics graphics, @NonNull INode parentNode, |
| @NonNull List<? extends INode> childNodes, @Nullable Object view) { |
| super.paintSelectionFeedback(graphics, parentNode, childNodes, view); |
| |
| if (sShowStructure) { |
| // TODO: Cache the grid |
| if (view != null) { |
| if (GridLayoutPainter.paintStructure(view, DrawingStyle.GUIDELINE_DASHED, |
| parentNode, graphics)) { |
| return; |
| } |
| } |
| GridLayoutPainter.paintStructure(DrawingStyle.GUIDELINE_DASHED, |
| parentNode, graphics, GridModel.get(mRulesEngine, parentNode, view)); |
| } else if (sDebugGridLayout) { |
| GridLayoutPainter.paintStructure(DrawingStyle.GRID, |
| parentNode, graphics, GridModel.get(mRulesEngine, parentNode, view)); |
| } |
| |
| // TBD: Highlight the cells around the selection, and display easy controls |
| // for for example tweaking the rowspan/colspan of a cell? (but only in grid mode) |
| } |
| |
| /** |
| * Paste into a GridLayout. We have several possible behaviors (and many |
| * more than are listed here): |
| * <ol> |
| * <li> Preserve the current positions of the elements (if pasted from another |
| * canvas, not just XML markup copied from say a web site) and apply those |
| * into the current grid. This might mean "overwriting" (sitting on top of) |
| * existing elements. |
| * <li> Fill available "holes" in the grid. |
| * <li> Lay them out consecutively, row by row, like text. |
| * <li> Some hybrid approach, where I attempt to preserve the <b>relative</b> |
| * relationships (columns/wrapping, spacing between the pasted views etc) |
| * but I append them to the bottom of the layout on one or more new rows. |
| * <li> Try to paste at the current mouse position, if known, preserving the |
| * relative distances between the existing elements there. |
| * </ol> |
| * Attempting to preserve the current position isn't possible right now, |
| * because the clipboard data contains only the textual representation of |
| * the markup. (We'd need to stash position information from a previous |
| * layout render along with the clipboard data). |
| * <p> |
| * Currently, this implementation simply lays out the elements row by row, |
| * approach #3 above. |
| */ |
| @Override |
| public void onPaste( |
| @NonNull INode targetNode, |
| @Nullable Object targetView, |
| @NonNull IDragElement[] elements) { |
| DropFeedback feedback = onDropEnter(targetNode, targetView, elements); |
| if (feedback != null) { |
| Rect b = targetNode.getBounds(); |
| if (!b.isValid()) { |
| return; |
| } |
| |
| Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements, |
| true /* remap id's */); |
| |
| for (IDragElement element : elements) { |
| // Skip <Space> elements and only insert the real elements being |
| // copied |
| if (elements.length > 1 && (FQCN_SPACE.equals(element.getFqcn()) |
| || FQCN_SPACE_V7.equals(element.getFqcn()))) { |
| continue; |
| } |
| |
| String fqcn = element.getFqcn(); |
| INode newChild = targetNode.appendChild(fqcn); |
| addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER); |
| |
| // Ensure that we reset any potential row/column attributes from a different |
| // grid layout being copied from |
| GridDropHandler handler = (GridDropHandler) feedback.userData; |
| GridModel grid = handler.getGrid(); |
| grid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, null); |
| grid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, null); |
| |
| // TODO: Set columnSpans to avoid making these widgets completely |
| // break the layout |
| // Alternatively, I could just lay them all out on subsequent lines |
| // with a column span of columnSpan5 |
| |
| addInnerElements(newChild, element, idMap); |
| } |
| } |
| } |
| } |