blob: a2e3fcaf010b33ef4140c694228f16f532407c1b [file] [log] [blame]
/*
* Copyright (C) 2015 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.uibuilder.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.idea.rendering.RenderTask;
import com.android.tools.idea.uibuilder.graphics.NlDrawingStyle;
import com.android.tools.idea.uibuilder.graphics.NlGraphics;
import com.android.tools.idea.uibuilder.model.*;
import com.intellij.psi.xml.XmlTag;
import java.awt.*;
import java.util.Map;
import static com.android.SdkConstants.*;
import static com.android.tools.idea.uibuilder.graphics.NlConstants.MAX_MATCH_DISTANCE;
/**
* Default implementation of a {@link ResizeHandler} which provides
* basic resizing (setting layout_width/layout_height to match_parent/
* wrap_content/Ndp.
*/
public class DefaultResizeHandler extends ResizeHandler {
/** The proposed resized bounds of the node */
public Rectangle bounds;
/** The preferred wrap_content bounds of the node */
public Dimension wrapBounds;
/** Whether the user has snapped to the wrap_content width */
public boolean wrapWidth;
/** Whether the user has snapped to the wrap_content height */
public boolean wrapHeight;
/** Whether the user has snapped to the match_parent width */
public boolean fillWidth;
/** Whether the user has snapped to the match_parent height */
public boolean fillHeight;
/** The suggested horizontal fill_parent guideline position */
public Segment horizontalFillSegment;
/** The suggested vertical fill_parent guideline position */
public Segment verticalFillSegment;
/**
* Constructs a new resize handler to resize the given component
*
* @param editor the associated IDE editor
* @param handler the view group handler that may receive the dragged components
* @param component the component being resized
* @param horizontalEdgeType the horizontal (top or bottom) edge being resized, if any
* @param verticalEdgeType the vertical (left or right) edge being resized, if any
*/
public DefaultResizeHandler(@NonNull ViewEditor editor,
@NonNull ViewGroupHandler handler,
@NonNull NlComponent component,
@Nullable SegmentType horizontalEdgeType,
@Nullable SegmentType verticalEdgeType) {
super(editor, handler, component, horizontalEdgeType, verticalEdgeType);
Map<NlComponent, Dimension> sizes = editor.measureChildren(layout, new RenderTask.AttributeFilter() {
@Override
public String getAttribute(@NonNull XmlTag n, @Nullable String namespace, @NonNull String localName) {
// Change attributes to wrap_content
if (ATTR_LAYOUT_WIDTH.equals(localName) && ANDROID_URI.equals(namespace)) {
return VALUE_WRAP_CONTENT;
}
if (ATTR_LAYOUT_HEIGHT.equals(localName) && ANDROID_URI.equals(namespace)) {
return VALUE_WRAP_CONTENT;
}
return null;
}
});
if (sizes != null) {
wrapBounds = sizes.get(component);
}
}
@Nullable
@Override
public String update(@AndroidCoordinate int x,
@AndroidCoordinate int y,
int modifiers,
@NonNull @AndroidCoordinate Rectangle newBounds) {
super.update(x, y, modifiers, newBounds);
bounds = newBounds;
// Match on wrap bounds
wrapWidth = wrapHeight = false;
if (wrapBounds != null) {
Dimension b = wrapBounds;
int maxMatchDistance = MAX_MATCH_DISTANCE;
if (horizontalEdgeType != null) {
if (Math.abs(newBounds.height - b.height) < maxMatchDistance) {
wrapHeight = true;
if (horizontalEdgeType == SegmentType.TOP) {
newBounds.y += newBounds.height - b.height;
}
newBounds.height = b.height;
}
}
if (verticalEdgeType != null) {
if (Math.abs(newBounds.width - b.width) < maxMatchDistance) {
wrapWidth = true;
if (verticalEdgeType == SegmentType.LEFT) {
newBounds.x += newBounds.width - b.width;
}
newBounds.width = b.width;
}
}
}
// Match on fill bounds
horizontalFillSegment = null;
fillHeight = false;
Rectangle parentBounds = new Rectangle(layout.x, layout.y, layout.w, layout.h);
if (horizontalEdgeType == SegmentType.BOTTOM && !wrapHeight) {
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) {
fillHeight = true;
newBounds.height = parentBounds.y + parentBounds.height - newBounds.y;
}
}
verticalFillSegment = null;
fillWidth = false;
if (verticalEdgeType == SegmentType.RIGHT && !wrapWidth) {
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) {
fillWidth = true;
newBounds.width = parentBounds.x + parentBounds.width - newBounds.x;
}
}
return null;
}
@Override
public void commit(@AndroidCoordinate int px,
@AndroidCoordinate int py,
int modifiers,
@NonNull @AndroidCoordinate Rectangle newBounds) {
NlComponent parent = component.getParent();
if (parent == null) {
return;
}
setNewSizeBounds(component, parent, new Rectangle(component.x, component.y, component.w, component.h),
newBounds, horizontalEdgeType, verticalEdgeType);
}
@Override
public void paint(@NonNull NlGraphics graphics) {
graphics.useStyle(NlDrawingStyle.RESIZE_PREVIEW);
if (bounds == null) {
return;
}
Rectangle b = bounds;
graphics.drawRect(b.x, b.y, b.width, b.height);
if (horizontalFillSegment != null) {
graphics.useStyle(NlDrawingStyle.GUIDELINE);
Segment s = horizontalFillSegment;
graphics.drawLine(s.from, s.at, s.to, s.at);
}
if (verticalFillSegment != null) {
graphics.useStyle(NlDrawingStyle.GUIDELINE);
Segment s = verticalFillSegment;
graphics.drawLine(s.at, s.from, s.at, s.to);
}
if (wrapBounds != null) {
graphics.useStyle(NlDrawingStyle.GUIDELINE);
int wrapWidth1 = wrapBounds.width;
int wrapHeight1 = wrapBounds.height;
// Show the "wrap_content" guideline.
// If we are showing both the wrap_width and wrap_height lines
// then we show at most the rectangle formed by the two lines;
// otherwise we show the entire width of the line
if (horizontalEdgeType != null) {
int y = -1;
switch (horizontalEdgeType) {
case TOP:
y = b.y + b.height - wrapHeight1;
break;
case BOTTOM:
y = b.y + wrapHeight1;
break;
default: assert false : horizontalEdgeType;
}
if (verticalEdgeType != null) {
switch (verticalEdgeType) {
case LEFT:
graphics.drawLine(b.x + b.width - wrapWidth1, y, b.x + b.width, y);
break;
case RIGHT:
graphics.drawLine(b.x, y, b.x + wrapWidth1, y);
break;
default: assert false : verticalEdgeType;
}
} else {
graphics.drawLine(b.x, y, b.x + b.width, y);
}
}
if (verticalEdgeType != null) {
int x = -1;
switch (verticalEdgeType) {
case LEFT:
x = b.x + b.width - wrapWidth1;
break;
case RIGHT:
x = b.x + wrapWidth1;
break;
default: assert false : verticalEdgeType;
}
if (horizontalEdgeType != null) {
switch (horizontalEdgeType) {
case TOP:
graphics.drawLine(x, b.y + b.height - wrapHeight1, x, b.y + b.height);
break;
case BOTTOM:
graphics.drawLine(x, b.y, x, b.y + wrapHeight1);
break;
default: assert false : horizontalEdgeType;
}
} else {
graphics.drawLine(x, b.y, x, b.y + b.height);
}
}
}
}
/**
* Returns the width attribute to be set to match the new bounds
*
* @return the width string, never null
*/
@NonNull
public String getWidthAttribute() {
if (wrapWidth) {
return VALUE_WRAP_CONTENT;
} else if (fillWidth) {
return VALUE_MATCH_PARENT;
} else {
return String.format(VALUE_N_DP, editor.pxToDp(bounds.width));
}
}
/**
* Returns the height attribute to be set to match the new bounds
*
* @return the height string, never null
*/
@NonNull
public String getHeightAttribute() {
if (wrapHeight) {
return VALUE_WRAP_CONTENT;
} else if (fillHeight) {
return VALUE_MATCH_PARENT;
} else {
return String.format(VALUE_N_DP, editor.pxToDp(bounds.height));
}
}
/**
* Returns the message to display to the user during the resize operation
*
* @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
*/
@Nullable
protected String getResizeUpdateMessage(@NonNull NlComponent child,
@NonNull NlComponent parent,
@NonNull Rectangle newBounds,
@Nullable SegmentType horizontalEdge,
@Nullable SegmentType verticalEdge) {
String width = getWidthAttribute();
String height = 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);
}
}
/**
* 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 component 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(@NonNull NlComponent component,
@NonNull NlComponent layout,
@NonNull Rectangle oldBounds,
@NonNull Rectangle newBounds,
@Nullable SegmentType horizontalEdge,
@Nullable SegmentType verticalEdge) {
if (verticalEdge != null
&& (newBounds.width != oldBounds.width || wrapWidth || fillWidth)) {
component.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, getWidthAttribute());
}
if (horizontalEdge != null
&& (newBounds.height != oldBounds.height || wrapHeight || fillHeight)) {
component.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, getHeightAttribute());
}
}
}