/* * 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.getPlatform(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); } } } }