/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 * * 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.tools.idea.designer; import com.android.ide.common.rendering.api.ViewInfo; import com.android.tools.idea.rendering.RenderService; import com.intellij.android.designer.AndroidDesignerUtils; import com.intellij.android.designer.designSurface.feedbacks.TextFeedback; import com.intellij.android.designer.designSurface.graphics.DirectionResizePoint; import com.intellij.android.designer.designSurface.graphics.DrawingStyle; import com.intellij.android.designer.designSurface.graphics.RectangleFeedback; import com.intellij.android.designer.designSurface.graphics.ResizeSelectionDecorator; import com.intellij.android.designer.model.RadViewComponent; import com.intellij.designer.designSurface.EditOperation; import com.intellij.designer.designSurface.FeedbackLayer; import com.intellij.designer.designSurface.OperationContext; import com.intellij.designer.designSurface.feedbacks.LineMarginBorder; import com.intellij.designer.model.RadComponent; import com.intellij.designer.utils.Position; import com.intellij.openapi.application.ApplicationManager; import com.intellij.psi.xml.XmlTag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.awt.*; import java.awt.event.InputEvent; import java.util.List; import static com.android.SdkConstants.*; import static com.intellij.android.designer.designSurface.graphics.DrawingStyle.MAX_MATCH_DISTANCE; public class ResizeOperation implements EditOperation { public static final String TYPE = "resize_children"; private static final String LABEL_CHANGE_BOTH = "Change layout:width x layout:height"; private static final String LABEL_CHANGE_WIDTH = "Change layout:width"; private static final String LABEL_CHANGE_HEIGHT = "Change layout:height"; protected ResizeContext myResizeContext; protected final OperationContext myContext; protected RadViewComponent myComponent; protected TextFeedback myTextFeedback; private RectangleFeedback myWrapFeedback; private RectangleFeedback myFillFeedback; private RectangleFeedback myFeedback; public ResizeOperation(OperationContext context) { myContext = context; } /** * For the new mouse position, compute the resized bounds (the bounding rectangle that * the view should be resized to). This is not just a width or height, since in some * cases resizing will change the x/y position of the view as well (for example, in * RelativeLayout or in AbsoluteLayout). */ private Rectangle getResizedBounds() { // Similar to myContext.getTransformedRectangle(myComponent.getBounds()), but handles // aspect-preserving resizing etc Rectangle b = myComponent.getBounds(); Dimension sizeDelta = myContext.getSizeDelta(); FeedbackLayer layer = myContext.getArea().getFeedbackLayer(); sizeDelta = myComponent.toModel(layer, sizeDelta); int direction = myContext.getResizeDirection(); int x = b.x; int y = b.y; int w = b.width; int h = b.height; int newW = b.width + sizeDelta.width; int newH = b.height + sizeDelta.height; ResizePolicy resizePolicy = ResizePolicy.getResizePolicy(myComponent); if (resizePolicy.isAspectPreserving() && w != 0 && h != 0 && (myResizeContext.modifierMask & InputEvent.SHIFT_MASK) == 0) { double aspectRatio = w / (double) h; double newAspectRatio = newW / (double) newH; if (newH == 0 || newAspectRatio > aspectRatio) { newH = (int)(newW / aspectRatio); } else { newW = (int)(newH * aspectRatio); } switch (direction) { case Position.SOUTH: direction = Position.SOUTH_EAST; myResizeContext.verticalEdgeType = SegmentType.RIGHT; break; case Position.NORTH: direction = Position.NORTH_EAST; myResizeContext.verticalEdgeType = SegmentType.RIGHT; break; case Position.EAST: direction = Position.SOUTH_EAST; myResizeContext.horizontalEdgeType = SegmentType.BOTTOM; break; case Position.WEST: direction = Position.SOUTH_WEST; myResizeContext.horizontalEdgeType = SegmentType.BOTTOM; break; } } if (isLeft(direction)) { // The user is dragging the left edge, so the position is anchored on the right. int x2 = b.x + b.width; w = newW; x = x2 - newW; } else if (isRight(direction)) { // The user is dragging the right edge, so the position is anchored on the left. w = newW; } else { assert direction == Position.SOUTH || direction == Position.NORTH : direction; } if (isTop(direction)) { // The user is dragging the top edge, so the position is anchored on the bottom. int y2 = b.y + b.height; h = newH; y = y2 - newH; } else if (isBottom(direction)) { // The user is dragging the bottom edge, so the position is anchored on the top. h = newH; } else { assert direction == Position.WEST || direction == Position.EAST : direction; } return new Rectangle(x, y, Math.max(w, 0), Math.max(h, 0)); } @Override public void setComponents(List<RadComponent> components) { } @Override public void setComponent(RadComponent component) { myComponent = (RadViewComponent)component; init(); } private void init() { assert myComponent != null; RadViewComponent layout = (RadViewComponent)myComponent.getParent(); int direction1 = myContext.getResizeDirection(); Object layoutView = layout.getViewInfo() != null ? layout.getViewInfo().getViewObject() : null; myResizeContext = createResizeContext(layout, layoutView, myComponent); myResizeContext.bounds = myComponent.getBounds(); myResizeContext.horizontalEdgeType = SegmentType.getHorizontalResizeEdge(direction1); myResizeContext.verticalEdgeType = SegmentType.getVerticalResizeEdge(direction1); FeedbackLayer layer = myContext.getArea().getFeedbackLayer(); Rectangle bounds = myComponent.getBounds(layer); Dimension fillSize = myComponent.fromModel(layer, myResizeContext.fillSize); Dimension wrapSize = myComponent.fromModel(layer, myResizeContext.wrapSize); int direction = myContext.getResizeDirection(); int wrapX = bounds.x; int wrapY = bounds.y; int fillWidth = fillSize.width; int fillHeight = fillSize.height; if (isLeft(direction)) { // The user is dragging the left edge, so the position is anchored on the // right. wrapX = bounds.x + bounds.width - wrapSize.width; } else if (isRight(direction)) { // The user is dragging the right edge, so the position is anchored on the // left. wrapX = bounds.x; } else { assert direction == Position.SOUTH || direction == Position.NORTH : direction; fillWidth = bounds.width; } if (isTop(direction)) { // The user is dragging the top edge, so the position is anchored on the // bottom. wrapY = bounds.y + bounds.height - wrapSize.height; } else if (isBottom(direction)) { // The user is dragging the bottom edge, so the position is anchored on the // top. wrapY = bounds.y; } else { assert direction == Position.WEST || direction == Position.EAST : direction; fillHeight = bounds.height; } Rectangle wrapBounds = new Rectangle(wrapX, wrapY, wrapSize.width, wrapSize.height); Rectangle fillBounds = new Rectangle(bounds.x, bounds.y, fillWidth, fillHeight); // Measure actual fill bounds RenderService service = AndroidDesignerUtils.getRenderService(myContext.getArea()); if (service != null) { final XmlTag tag = myComponent.getTag(); ViewInfo viewInfo = service.measureChild(tag, new RenderService.AttributeFilter() { @Override public String getAttribute(@NotNull XmlTag n, @Nullable String namespace, @NotNull String name) { // Clear out layout weights; we need to measure the unweighted sizes // of the children if (n == tag && (ATTR_LAYOUT_WIDTH.equals(name) || ATTR_LAYOUT_HEIGHT.equals(name)) && ANDROID_URI.equals(namespace)) { return VALUE_FILL_PARENT; } return null; } }); if (viewInfo != null) { int left = viewInfo.getLeft(); int top = viewInfo.getTop(); fillBounds = new Rectangle(left, top, viewInfo.getRight() - left, viewInfo.getBottom() - top); // Translate from Android model coordinates to designer UI coordinates fillBounds = myComponent.fromModel(layer, fillBounds); } } myWrapFeedback = new RectangleFeedback(DrawingStyle.RESIZE_WRAP); myWrapFeedback.setBounds(wrapBounds); myFillFeedback = new RectangleFeedback(DrawingStyle.GUIDELINE_DASHED); myFillFeedback.setBounds(fillBounds); } private void createFeedback() { if (myFeedback == null) { FeedbackLayer layer = myContext.getArea().getFeedbackLayer(); myFeedback = new RectangleFeedback(DrawingStyle.RESIZE_PREVIEW); layer.add(myFeedback); myTextFeedback = new TextFeedback(); myTextFeedback.setBorder(new LineMarginBorder(0, 5, 3, 0)); layer.add(myTextFeedback); layer.add(myWrapFeedback); layer.add(myFillFeedback); layer.repaint(); onResizeBegin(); } } /** Is the direction somewhere on the left edge? */ private static boolean isLeft(int direction) { return direction == Position.NORTH_WEST || direction == Position.WEST || direction == Position.SOUTH_WEST; } /** Is the direction somewhere on the right edge? */ private static boolean isRight(int direction) { return direction == Position.NORTH_EAST || direction == Position.EAST || direction == Position.SOUTH_EAST; } /** Is the direction somewhere on the top edge? */ private static boolean isTop(int direction) { return direction == Position.NORTH_WEST || direction == Position.NORTH || direction == Position.NORTH_EAST; } /** Is the direction somewhere on the bottom edge? */ private static boolean isBottom(int direction) { return direction == Position.SOUTH_WEST || direction == Position.SOUTH || direction == Position.SOUTH_EAST; } /** Creates a new {@link ResizeContext} object to track resize state */ protected ResizeContext createResizeContext(RadViewComponent layout, @Nullable Object layoutView, RadViewComponent node) { return new ResizeContext(myContext.getArea(), layout, layoutView, node); } /** * Performs the edit on the node to complete a resizing operation. The actual edit * part is pulled out such that subclasses can change/add to the edits and be part of * the same undo event * * @param resizeContext the current resize state * @param node the child node being resized * @param layout the parent of the resized node * @param newBounds the new bounds to resize the child to, in pixels * @param horizontalEdge the horizontal edge being resized * @param verticalEdge the vertical edge being resized */ protected void setNewSizeBounds(ResizeContext resizeContext, RadViewComponent node, RadViewComponent layout, Rectangle oldBounds, Rectangle newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { if (verticalEdge != null && (newBounds.width != oldBounds.width || resizeContext.wrapWidth || resizeContext.fillWidth)) { node.setAttribute(ATTR_LAYOUT_WIDTH, ANDROID_URI, resizeContext.getWidthAttribute()); } if (horizontalEdge != null && (newBounds.height != oldBounds.height || resizeContext.wrapHeight || resizeContext.fillHeight)) { node.setAttribute(ATTR_LAYOUT_HEIGHT, ANDROID_URI, resizeContext.getHeightAttribute()); } } /** * Returns the message to display to the user during the resize operation * * @param resizeContext the current resize state * @param child the child node being resized * @param parent the parent of the resized node * @param newBounds the new bounds to resize the child to, in pixels * @param horizontalEdge the horizontal edge being resized * @param verticalEdge the vertical edge being resized * @return the message to display for the current resize bounds */ protected String getResizeUpdateMessage(ResizeContext resizeContext, RadViewComponent child, RadViewComponent parent, Rectangle newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { String width = resizeContext.getWidthAttribute(); String height = resizeContext.getHeightAttribute(); if (horizontalEdge == null) { return width; } else if (verticalEdge == null) { return height; } else { // U+00D7: Unicode for multiplication sign return String.format("%s \u00D7 %s", width, height); } } public void onResizeBegin() { } public void onResizeUpdate(@NotNull RadViewComponent parent, @NotNull Rectangle newBounds, int modifierMask) { myResizeContext.bounds = newBounds; myResizeContext.modifierMask = modifierMask; // Match on wrap bounds myResizeContext.wrapWidth = myResizeContext.wrapHeight = false; if (myResizeContext.wrapSize != null) { Dimension b = myResizeContext.wrapSize; if (myResizeContext.horizontalEdgeType != null) { if (Math.abs(newBounds.height - b.height) < MAX_MATCH_DISTANCE) { myResizeContext.wrapHeight = true; if (myResizeContext.horizontalEdgeType == SegmentType.TOP) { newBounds.y += newBounds.height - b.height; } newBounds.height = b.height; } } if (myResizeContext.verticalEdgeType != null) { if (Math.abs(newBounds.width - b.width) < MAX_MATCH_DISTANCE) { myResizeContext.wrapWidth = true; if (myResizeContext.verticalEdgeType == SegmentType.LEFT) { newBounds.x += newBounds.width - b.width; } newBounds.width = b.width; } } } // Match on fill bounds myResizeContext.horizontalFillSegment = null; myResizeContext.fillHeight = false; if (myResizeContext.horizontalEdgeType == SegmentType.BOTTOM && !myResizeContext.wrapHeight) { Rectangle parentBounds = parent.getBounds(); myResizeContext.horizontalFillSegment = new Segment(parentBounds.y + parentBounds.height, newBounds.x, newBounds.x + newBounds.width, null /*node*/, null /*id*/, SegmentType.BOTTOM, MarginType.NO_MARGIN); if (Math.abs(newBounds.y + newBounds.height - (parentBounds.y + parentBounds.height)) < MAX_MATCH_DISTANCE) { myResizeContext.fillHeight = true; newBounds.height = parentBounds.y + parentBounds.height - newBounds.y; } } myResizeContext.verticalFillSegment = null; myResizeContext.fillWidth = false; if (myResizeContext.verticalEdgeType == SegmentType.RIGHT && !myResizeContext.wrapWidth) { Rectangle parentBounds = parent.getBounds(); myResizeContext.verticalFillSegment = new Segment(parentBounds.x + parentBounds.width, newBounds.y, newBounds.y + newBounds.height, null /*node*/, null /*id*/, SegmentType.RIGHT, MarginType.NO_MARGIN); if (Math.abs(newBounds.x + newBounds.width - (parentBounds.x + parentBounds.width)) < MAX_MATCH_DISTANCE) { myResizeContext.fillWidth = true; newBounds.width = parentBounds.x + parentBounds.width - newBounds.x; } } } protected void updateResizeMessage() { RadViewComponent layout = (RadViewComponent)myComponent.getParent(); String message = getResizeUpdateMessage(myResizeContext, myComponent, layout, myResizeContext.bounds, myResizeContext.horizontalEdgeType, myResizeContext.verticalEdgeType); myTextFeedback.append(message); myTextFeedback.setSize(myTextFeedback.getPreferredSize()); myTextFeedback.locationTo(myContext.getLocation(), 15); } @Override public void showFeedback() { createFeedback(); FeedbackLayer layer = myContext.getArea().getFeedbackLayer(); Rectangle modelBounds = getResizedBounds(); RadViewComponent layout = (RadViewComponent)myComponent.getParent(); onResizeUpdate(layout, modelBounds, myContext.getModifiers()); Rectangle viewBounds = myComponent.fromModel(layer, myResizeContext.bounds); myFeedback.setBounds(viewBounds); myTextFeedback.clear(); updateResizeMessage(); } @Override public void eraseFeedback() { if (myFeedback != null) { FeedbackLayer layer = myContext.getArea().getFeedbackLayer(); layer.remove(myFeedback); layer.remove(myTextFeedback); layer.remove(myWrapFeedback); layer.remove(myFillFeedback); layer.repaint(); myFeedback = null; myTextFeedback = null; myWrapFeedback = null; myFillFeedback = null; } } @Override public boolean canExecute() { return true; } @Override public void execute() throws Exception { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { RadViewComponent layout = (RadViewComponent)myComponent.getParent(); Rectangle oldBounds = myComponent.getBounds(); Rectangle newBounds = getResizedBounds(); setNewSizeBounds(myResizeContext, myComponent, layout, oldBounds, newBounds, myResizeContext.horizontalEdgeType, myResizeContext.verticalEdgeType); } }); } public static void addResizePoints(ResizeSelectionDecorator decorator) { addResizePoints(decorator, ResizePolicy.full()); } public static void addResizePoints(ResizeSelectionDecorator decorator, @NotNull RadViewComponent component) { addResizePoints(decorator, ResizePolicy.getResizePolicy(component)); } public static void addResizePoints(ResizeSelectionDecorator decorator, @NotNull ResizePolicy policy) { if (policy.leftAllowed()) { decorator.addPoint(new DirectionResizePoint(DrawingStyle.SELECTION, Position.WEST, TYPE, LABEL_CHANGE_WIDTH)); if (policy.topAllowed()) { decorator.addPoint(new DirectionResizePoint(DrawingStyle.SELECTION, Position.NORTH_WEST, TYPE, LABEL_CHANGE_BOTH)); } if (policy.bottomAllowed()) { decorator.addPoint(new DirectionResizePoint(DrawingStyle.SELECTION, Position.SOUTH_WEST, TYPE, LABEL_CHANGE_BOTH)); } } if (policy.rightAllowed()) { decorator.addPoint(new DirectionResizePoint(DrawingStyle.SELECTION, Position.EAST, TYPE, LABEL_CHANGE_WIDTH)); if (policy.topAllowed()) { decorator.addPoint(new DirectionResizePoint(DrawingStyle.SELECTION, Position.NORTH_EAST, TYPE, LABEL_CHANGE_BOTH)); } if (policy.bottomAllowed()) { decorator.addPoint(new DirectionResizePoint(DrawingStyle.SELECTION, Position.SOUTH_EAST, TYPE, LABEL_CHANGE_BOTH)); } } if (policy.topAllowed()) { decorator.addPoint(new DirectionResizePoint(DrawingStyle.SELECTION, Position.NORTH, TYPE, LABEL_CHANGE_HEIGHT)); } if (policy.bottomAllowed()) { decorator.addPoint(new DirectionResizePoint(DrawingStyle.SELECTION, Position.SOUTH, TYPE, LABEL_CHANGE_HEIGHT)); } } }