blob: 4ea416d1434389af8125f1ecd01485989a8507af [file] [log] [blame]
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* 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.intellij.android.designer.designSurface.layout;
import com.android.tools.idea.designer.FillPolicy;
import com.android.tools.idea.designer.ResizeContext;
import com.intellij.android.designer.AndroidDesignerUtils;
import com.intellij.android.designer.designSurface.AndroidDesignerEditorPanel;
import com.intellij.android.designer.designSurface.feedbacks.TextFeedback;
import com.intellij.android.designer.designSurface.graphics.DesignerGraphics;
import com.intellij.android.designer.designSurface.graphics.DrawingStyle;
import com.intellij.android.designer.designSurface.layout.flow.FlowBaseOperation;
import com.intellij.android.designer.model.RadComponentOperations;
import com.intellij.android.designer.model.RadViewComponent;
import com.intellij.android.designer.model.layout.Gravity;
import com.intellij.designer.designSurface.FeedbackLayer;
import com.intellij.designer.designSurface.OperationContext;
import com.intellij.designer.model.RadComponent;
import com.intellij.designer.palette.PaletteItem;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlTag;
import com.intellij.ui.IdeBorderFactory;
import org.jetbrains.android.sdk.AndroidPlatform;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.util.List;
import static com.android.SdkConstants.*;
import static com.intellij.android.designer.designSurface.graphics.DrawingStyle.SHOW_STATIC_GRID;
/**
* @author Alexander Lobas
*/
public class LinearLayoutOperation extends FlowBaseOperation {
private GravityFeedback myFeedback;
private TextFeedback myTextFeedback;
private FlowPositionFeedback myFlowFeedback;
private Gravity myExclude;
private Gravity myGravity;
private boolean mySetGravity;
public LinearLayoutOperation(RadComponent container, OperationContext context, boolean horizontal) {
super(container, context, horizontal);
if (context.isMove() && context.getComponents().size() == 1) {
myExclude = getGravity(myHorizontal, context.getComponents().get(0));
}
}
private final static int MIN_MARGIN_DIMENSIONS = 100;
@Override
protected void createFeedback() {
super.createFeedback();
if (myTextFeedback == null) {
FeedbackLayer layer = myContext.getArea().getFeedbackLayer();
if (myHorizontal && myBounds.height > MIN_MARGIN_DIMENSIONS
|| !myHorizontal && myBounds.width > MIN_MARGIN_DIMENSIONS) {
mySetGravity = true;
myFeedback = new GravityFeedback();
if (myContainer.getChildren().isEmpty()) {
myFeedback.setBounds(myBounds);
}
layer.add(myFeedback, 0);
}
//noinspection ConstantConditions
if (!SHOW_STATIC_GRID) {
myFlowFeedback = new FlowPositionFeedback();
myFlowFeedback.setBounds(myBounds);
layer.add(myFlowFeedback, 0);
}
myTextFeedback = new TextFeedback();
myTextFeedback.setBorder(IdeBorderFactory.createEmptyBorder(0, 3, 2, 0));
layer.add(myTextFeedback);
layer.repaint();
}
}
@Override
public void showFeedback() {
super.showFeedback();
if (mySetGravity) {
Point location = myContext.getLocation();
Gravity gravity = myHorizontal ? calculateVertical(myBounds, location) : calculateHorizontal(myBounds, location);
if (gravity == null) {
gravity = myHorizontal ? Gravity.left : Gravity.top;
}
if (!myContainer.getChildren().isEmpty()) {
myFeedback.setBounds(myInsertFeedback.getBounds());
}
myFeedback.setGravity(gravity);
myTextFeedback.clear();
if (gravity == Gravity.left || gravity == Gravity.top) {
myTextFeedback.setVisible(false);
} else {
myTextFeedback.bold(gravity.name());
myTextFeedback.setVisible(true);
}
myTextFeedback.centerTop(myBounds);
myGravity = gravity;
}
//noinspection ConstantConditions
if (!SHOW_STATIC_GRID) {
myFlowFeedback.repaint();
}
}
@Override
public void eraseFeedback() {
super.eraseFeedback();
if (myTextFeedback != null) {
FeedbackLayer layer = myContext.getArea().getFeedbackLayer();
if (mySetGravity) {
layer.remove(myFeedback);
}
layer.remove(myTextFeedback);
//noinspection ConstantConditions
if (!SHOW_STATIC_GRID) {
layer.remove(myFlowFeedback);
}
layer.repaint();
myFeedback = null;
myTextFeedback = null;
}
}
@Nullable
private Gravity calculateHorizontal(Rectangle bounds, Point location) {
// Only align to the bottom if you're within the final quarter of the width (*and* the dragged bounds are significantly
// smaller than the available bounds)
List<RadComponent> dragged = myContext.getComponents();
assert !dragged.isEmpty();
if (dragged.size() > 1) {
return Gravity.left;
}
RadComponent component = dragged.get(0);
if (component.getBounds(myContext.getArea().getFeedbackLayer()).width > bounds.width / 2) {
return Gravity.left;
}
if (isFilled(myHorizontal, (RadViewComponent)component)) {
return Gravity.left;
}
int thirds = bounds.width / 3;
int right = bounds.x + 2 * thirds;
if (location.x >= right) {
return Gravity.right;
}
int center = bounds.x + thirds;
if (location.x >= center) {
return Gravity.center;
}
return Gravity.left;
}
@Nullable
private Gravity calculateVertical(Rectangle bounds, Point location) {
// Only align to the bottom if you're within the final quarter of the height (*and* the dragged bounds are significantly
// smaller than the available bounds)
List<RadComponent> dragged = myContext.getComponents();
assert !dragged.isEmpty();
if (dragged.size() > 1) {
return Gravity.top;
}
RadComponent component = dragged.get(0);
if (component.getBounds(myContext.getArea().getFeedbackLayer()).height > bounds.height / 2) {
return Gravity.top;
}
if (isFilled(myHorizontal, (RadViewComponent)component)) {
return Gravity.top;
}
int thirds = bounds.height / 3;
int bottom = bounds.y + 2 * thirds;
if (location.y >= bottom) {
return Gravity.bottom;
}
int center = bounds.y + thirds;
if (location.y >= center) {
return Gravity.center;
}
return Gravity.top;
}
@Override
public boolean canExecute() {
return super.canExecute() || (myComponents.size() == 1 && myGravity != myExclude);
}
@Override
public void execute() throws Exception {
if (super.canExecute()) {
super.execute();
}
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
Gravity gravity = myGravity == Gravity.top || myGravity == Gravity.left ? null : myGravity;
execute(myHorizontal, gravity, RadViewComponent.getViewComponents(myComponents));
}
});
}
public static void applyGravity(boolean horizontal, @Nullable Gravity gravity, @NotNull List<? extends RadViewComponent> components) {
if (gravity == null) {
for (RadViewComponent component : components) {
XmlTag tag = component.getTag();
RadComponentOperations.deleteAttribute(tag, ATTR_LAYOUT_GRAVITY);
}
}
else {
String gravityValue = horizontal ? Gravity.getValue(null, gravity) : Gravity.getValue(gravity, null);
for (RadViewComponent component : components) {
XmlTag tag = component.getTag();
if (isFilled(horizontal, component)) {
tag.setAttribute(horizontal ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH, ANDROID_URI, VALUE_WRAP_CONTENT);
}
if (gravityValue != null) {
tag.setAttribute(ATTR_LAYOUT_GRAVITY, ANDROID_URI, gravityValue);
} else {
XmlAttribute a = tag.getAttribute(ATTR_LAYOUT_GRAVITY, ANDROID_URI);
if (a != null) {
a.delete();
}
}
}
}
}
/**
* Returns fill_parent or match_parent, depending on whether the minimum supported
* platform supports match_parent or not
*
* @return match_parent or fill_parent depending on which is supported by the project
*/
protected final String getFillParentValueName() {
return supportsMatchParent() ? VALUE_MATCH_PARENT : VALUE_FILL_PARENT;
}
/**
* Returns true if the project supports match_parent instead of just fill_parent
*
* @return true if the project supports match_parent instead of just fill_parent
*/
@SuppressWarnings("MethodMayBeStatic")
protected final boolean supportsMatchParent() {
// fill_parent was renamed match_parent in API level 8
// Note that we check this on the build SDK, not the minSdkVersion; the constants
// have the same value, so as long as you compile on 8 or later (very likely)
// it will work on any version of the platform
AndroidDesignerEditorPanel panel = AndroidDesignerUtils.getPanel(myContext.getArea());
if (panel != null) {
AndroidPlatform platform = AndroidPlatform.getInstance(panel.getModule());
if (platform != null) {
return platform.getApiLevel() >= 8;
}
}
return true;
}
public void execute(boolean horizontal, @Nullable Gravity gravity, @NotNull List<? extends RadViewComponent> components) {
applyGravity(horizontal, gravity, components);
if (myContext.isMove()) {
// Don't adjust widths/heights/weights when just moving within a single
// LinearLayout
// TODO: If it's a move from one widget to another, track that differently!
return;
}
// Attempt to set fill-properties on newly added views such that for example,
// in a vertical layout, a text field defaults to filling horizontally, but not
// vertically.
if (components.size() == 1) {
RadViewComponent node = components.get(0);
boolean vertical = !horizontal;
FillPolicy fill = FillPolicy.getFillPreference(node);
String fillParent = getFillParentValueName();
XmlTag tag = node.getTag();
if (fill.fillHorizontally(vertical)) {
tag.setAttribute(ATTR_LAYOUT_WIDTH, ANDROID_URI, fillParent);
}
else {
tag.setAttribute(ATTR_LAYOUT_WIDTH, ANDROID_URI, VALUE_WRAP_CONTENT);
if (!vertical && fill == FillPolicy.WIDTH_IN_VERTICAL) {
// In a horizontal layout, make views that would fill horizontally in a
// vertical layout have a non-zero weight instead. This will make the item
// fill but only enough to allow other views to be shown as well.
// (However, for drags within the same layout we do not touch
// the weight, since it might already have been tweaked to a particular
// value)
tag.setAttribute(ATTR_LAYOUT_WEIGHT, ANDROID_URI, VALUE_1);
}
}
if (fill.fillVertically(vertical)) {
tag.setAttribute(ATTR_LAYOUT_HEIGHT, ANDROID_URI, fillParent);
} else {
tag.setAttribute(ATTR_LAYOUT_HEIGHT, ANDROID_URI, VALUE_WRAP_CONTENT);
}
// TODO: How does my measure-render handle drop customizations? I need to put it into the
// metadata!
}
// If you insert into a layout that already is using layout weights,
// and all the layout weights are the same (nonzero) value, then use
// the same weight for this new layout as well. Also duplicate the 0dip/0px/0dp
// sizes, if used.
boolean duplicateWeight = true;
boolean duplicate0dip = true;
String sameWeight = null;
String sizeAttribute = horizontal ? ATTR_LAYOUT_WIDTH : ATTR_LAYOUT_HEIGHT;
List<RadViewComponent> siblings = RadViewComponent.getViewComponents(myContainer.getChildren());
for (RadViewComponent target : siblings) {
if (components.contains(target)) {
continue;
}
XmlTag tag = target.getTag();
String weight = tag.getAttributeValue(ATTR_LAYOUT_WEIGHT, ANDROID_URI);
if (weight == null || weight.length() == 0) {
duplicateWeight = false;
break;
} else if (sameWeight != null && !sameWeight.equals(weight)) {
duplicateWeight = false;
} else {
sameWeight = weight;
}
String size = tag.getAttributeValue(sizeAttribute, ANDROID_URI);
if (size != null && !size.startsWith("0")) {
duplicate0dip = false;
break;
}
}
if (duplicateWeight && sameWeight != null) {
for (RadViewComponent component : components) {
XmlTag tag = component.getTag();
tag.setAttribute(ATTR_LAYOUT_WEIGHT, ANDROID_URI, sameWeight);
if (duplicate0dip) {
tag.setAttribute(sizeAttribute, ANDROID_URI, VALUE_ZERO_DP);
}
}
}
}
@Nullable
public static Gravity getGravity(boolean horizontal, RadComponent component) {
XmlTag tag = ((RadViewComponent)component).getTag();
String length = tag.getAttributeValue(horizontal ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH, ANDROID_URI);
if (length != null && !ResizeContext.isFill(length)) {
Pair<Gravity, Gravity> gravity = Gravity.getSides(component);
return horizontal ? gravity.second : gravity.first;
}
return null;
}
private static boolean isFilled(boolean horizontal, RadViewComponent component) {
XmlTag tag = component.getTag();
if (tag != null) {
// Dragging within canvas
XmlAttribute attribute = tag.getAttribute(horizontal ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH, ANDROID_URI);
if (attribute == null) {
return false;
}
String value = attribute.getValue();
return (VALUE_MATCH_PARENT.equals(value) || VALUE_FILL_PARENT.equals(value));
} else {
// Dragging from palette: no PSI element exists yet
// Look at the creation XML to see whether it's a filling view
PaletteItem paletteItem = component.getInitialPaletteItem();
if (paletteItem != null) {
String creation = paletteItem.getCreation();
if (creation != null) {
int index = creation.indexOf(horizontal ? "layout_width=\"wrap_content\"" : "layout_height=\"wrap_content\"");
return index == -1;
}
}
return false;
}
}
//////////////////////////////////////////////////////////////////////////////////////////
//
// Feedback
//
//////////////////////////////////////////////////////////////////////////////////////////
private class FlowPositionFeedback extends JComponent {
@Override
public void paint(Graphics graphics) {
super.paint(graphics);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Rectangle bounds = ((RadViewComponent)myContainer).getPaddedBounds(this);
RadComponent component = myContainer;
DesignerGraphics.drawRect(DrawingStyle.DROP_RECIPIENT, g, bounds.x, bounds.y, bounds.width, bounds.height);
if (myHorizontal) {
for (RadComponent child : component.getChildren()) {
Rectangle childBounds = child.getBounds(this);
int marginRight = ((RadViewComponent)child).getMargins(this).right;
int x = childBounds.x + childBounds.width + marginRight;
DesignerGraphics.drawLine(DrawingStyle.DROP_ZONE, g, x, bounds.y, x, bounds.y + bounds.height);
}
}
else {
for (RadComponent child : component.getChildren()) {
Rectangle childBounds = child.getBounds(this);
int marginBottom = ((RadViewComponent)child).getMargins(this).bottom;
int y = childBounds.y + childBounds.height + marginBottom;
DesignerGraphics.drawLine(DrawingStyle.DROP_ZONE, g, bounds.x, y, bounds.x + bounds.width, y);
}
}
}
}
private class GravityFeedback extends JComponent {
@Nullable private Gravity myGravity;
public void setGravity(@Nullable Gravity gravity) {
myGravity = gravity;
repaint();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// Paint outline of inserted component, if there's just one:
if (myComponents.size() == 1) {
RadViewComponent first = (RadViewComponent)myComponents.get(0);
Dimension size = AndroidDesignerUtils.computePreferredSize(myContext.getArea(), first, myContainer);
Rectangle bounds;
if (myHorizontal) {
bounds = computeHorizontalPreviewBounds(size);
}
else {
bounds = computeVerticalPreviewBounds(size);
}
Shape clip = g.getClip();
g.setClip(bounds);
DesignerGraphics.drawRect(DrawingStyle.DROP_PREVIEW, g, bounds.x, bounds.y, bounds.width, bounds.height);
g.setClip(clip);
} else {
// Multiple components: Just show insert line
if (myHorizontal) {
paintHorizontalCell(g);
}
else {
paintVerticalCell(g);
}
}
}
private Rectangle computeVerticalPreviewBounds(Dimension b) {
int x = 0;
int y = 0;
if (!myContainer.getChildren().isEmpty()) {
y -= b.height / 2;
}
if (myGravity == Gravity.center) {
x = getWidth() / 2 - b.width / 2;
}
else if (myGravity == null) {
x = 0;
b.width = getWidth();
}
else if (myGravity == Gravity.right) {
x = getWidth() - b.width;
}
int width = b.width;
int hSpace = Math.min(5, Math.max(1, getWidth() / 30));
if (hSpace > 1) {
x += hSpace;
width -= 2 * hSpace;
if (width < 5) {
width = 5;
}
}
return new Rectangle(x, y, width, b.height);
}
private Rectangle computeHorizontalPreviewBounds(Dimension b) {
int x = 0;
int y = 0;
if (!myContainer.getChildren().isEmpty()) {
x -= b.width / 2;
}
if (myGravity == Gravity.center) {
y = getHeight() / 2 - b.height / 2;
}
else if (myGravity == null) {
y = 0;
b.height = getHeight();
}
else if (myGravity == Gravity.bottom) {
y = getHeight() - b.height;
}
// Make the box slightly smaller such that it doesn't overlap exactly
// with the layout bounding box
int vSpace = Math.min(5, Math.max(1, getHeight() / 30));
int height = b.height;
if (vSpace > 1) {
y += vSpace;
height -= 2 * vSpace;
if (height < 5) {
height = 5;
}
}
return new Rectangle(x, y, b.width, height);
}
private void paintHorizontalCell(Graphics g) {
int y = 0;
int height = (getHeight() - 3) / 4;
if (myGravity == Gravity.center) {
y = height + 1;
}
else if (myGravity == null) {
y = 2 * height + 2;
}
else if (myGravity == Gravity.bottom) {
y = getHeight() - height;
}
int vSpace = Math.min(5, Math.max(1, getHeight() / 30));
if (vSpace > 1) {
y += vSpace;
height -= 2 * vSpace;
}
int thickness = DrawingStyle.GRAVITY.getLineWidth();
if (myContainer.getChildren().isEmpty()) {
DesignerGraphics.drawFilledRect(DrawingStyle.GRAVITY, g, 0, y, thickness, height);
DesignerGraphics.drawFilledRect(DrawingStyle.GRAVITY, g, myBounds.width - thickness, y, thickness, height);
}
else {
DesignerGraphics.drawLine(DrawingStyle.GRAVITY, g, thickness / 2, y, thickness / 2, y + height);
}
}
private void paintVerticalCell(Graphics g) {
int x = 0;
int width = (getWidth() - 3) / 4;
if (myGravity == Gravity.center) {
x = width + 1;
}
else if (myGravity == null) {
x = 2 * width + 2;
}
else if (myGravity == Gravity.right) {
x = getWidth() - width;
}
int hSpace = Math.min(5, Math.max(1, getWidth() / 30));
if (hSpace > 1) {
x += hSpace;
width -= 2 * hSpace;
}
int thickness = DrawingStyle.GRAVITY.getLineWidth();
if (myContainer.getChildren().isEmpty()) {
DesignerGraphics.drawFilledRect(DrawingStyle.GRAVITY, g, x, 0, width, thickness);
DesignerGraphics.drawFilledRect(DrawingStyle.GRAVITY, g, x, myBounds.height - thickness, width, thickness);
}
else {
DesignerGraphics.drawLine(DrawingStyle.GRAVITY, g, x, thickness / 2, x + width, thickness / 2);
}
}
}
}