diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java | 676 |
1 files changed, 676 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java new file mode 100644 index 000000000..80a23c6db --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java @@ -0,0 +1,676 @@ +/* + * 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); + } + } + } +} |