blob: 18c91ab849999f6f89405e9581b3cd077edbff1d [file] [log] [blame]
/*
* 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.RenderTask;
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
RenderTask task = AndroidDesignerUtils.createRenderTask(myContext.getArea());
if (task != null) {
final XmlTag tag = myComponent.getTag();
ViewInfo viewInfo = task.measureChild(tag, new RenderTask.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));
}
}
}