blob: 615e59bfeb27615f84bf61926d9a1cdd79c33ad9 [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.model;
import com.android.ide.common.rendering.api.ViewInfo;
import com.android.tools.idea.designer.AndroidMetaModel;
import com.android.tools.idea.designer.Insets;
import com.android.tools.idea.rendering.IncludeReference;
import com.android.tools.idea.rendering.RenderTask;
import com.intellij.android.designer.AndroidDesignerUtils;
import com.intellij.android.designer.designSurface.AndroidDesignerEditorPanel;
import com.intellij.android.designer.designSurface.TransformedComponent;
import com.intellij.designer.designSurface.EditableArea;
import com.intellij.designer.designSurface.ScalableComponent;
import com.intellij.designer.model.*;
import com.intellij.designer.palette.PaletteItem;
import com.intellij.designer.propertyTable.PropertyTable;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.containers.hash.HashMap;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.android.SdkConstants.*;
import static com.android.tools.idea.rendering.IncludeReference.ATTR_RENDER_IN;
/**
* @author Alexander Lobas
*/
public class RadViewComponent extends RadVisualComponent {
private final List<RadComponent> myChildren = new ArrayList<RadComponent>();
protected ViewInfo myViewInfo;
private Insets myMargins;
private Insets myPadding;
private XmlTag myTag;
private List<Property> myProperties;
private PaletteItem myPaletteItem;
public RadViewComponent() {
}
@NotNull
public XmlTag getTag() {
if (myTag == null || myTag.getParent() == null || !myTag.isValid()) {
return EmptyXmlTag.INSTANCE;
}
return myTag;
}
public void setTag(@Nullable XmlTag tag) {
myTag = tag;
}
@Nullable
public String getAttribute(@NotNull String name, @Nullable String namespace) {
if (namespace != null) {
return myTag.getAttributeValue(name, namespace);
} else {
return myTag.getAttributeValue(name);
}
}
public void setAttribute(@NotNull String name, @Nullable String namespace, @Nullable String value) {
if (namespace != null) {
//noinspection ConstantConditions
myTag.setAttribute(name, namespace, value);
} else {
//noinspection ConstantConditions
myTag.setAttribute(name, value);
}
}
public void updateTag(XmlTag tag) {
setTag(tag);
int size = myChildren.size();
XmlTag[] tags = tag.getSubTags();
for (int i = 0; i < size; i++) {
RadViewComponent child = (RadViewComponent)myChildren.get(i);
child.updateTag(tags[i]);
}
}
public String getCreationXml() {
throw new UnsupportedOperationException();
}
public ViewInfo getViewInfo() {
return myViewInfo;
}
public void setViewInfo(ViewInfo viewInfo) {
myViewInfo = viewInfo;
myMargins = null;
}
@Override
public AndroidMetaModel getMetaModel() {
return (AndroidMetaModel)super.getMetaModel();
}
//////////////////////////////////////////////////////////////////////////////////////////
//
//
//
//////////////////////////////////////////////////////////////////////////////////////////
public String ensureId() {
String id = getId();
if (id == null) {
id = IdManager.get().assignId(this);
}
return id;
}
@Nullable
public String getId() {
return StringUtil.nullize(getTag().getAttributeValue(ATTR_ID, ANDROID_URI), false);
}
//////////////////////////////////////////////////////////////////////////////////////////
//
//
//
//////////////////////////////////////////////////////////////////////////////////////////
public int getBaseline() {
try {
Object viewObject = myViewInfo.getViewObject();
return (Integer)viewObject.getClass().getMethod("getBaseline").invoke(viewObject);
}
catch (Throwable e) {
}
return -1;
}
@NotNull
public Insets getMargins() {
if (myMargins == null) {
try {
Object layoutParams = myViewInfo.getLayoutParamsObject();
Class<?> layoutClass = layoutParams.getClass();
int left = fixDefault(layoutClass.getField("leftMargin").getInt(layoutParams)); // TODO: startMargin?
int top = fixDefault(layoutClass.getField("topMargin").getInt(layoutParams));
int right = fixDefault(layoutClass.getField("rightMargin").getInt(layoutParams));
int bottom = fixDefault(layoutClass.getField("bottomMargin").getInt(layoutParams));
if (left == 0 && top == 0 && right == 0 && bottom == 0) {
myMargins = Insets.NONE;
} else {
myMargins = new Insets(left, top, right, bottom);
}
}
catch (Throwable e) {
myMargins = Insets.NONE;
}
}
return myMargins;
}
@NotNull
public Insets getPadding() {
if (myPadding == null) {
try {
Object layoutParams = myViewInfo.getViewObject();
Class<?> layoutClass = layoutParams.getClass();
int left = fixDefault((Integer)layoutClass.getMethod("getPaddingLeft").invoke(layoutParams)); // TODO: getPaddingStart!
int top = fixDefault((Integer)layoutClass.getMethod("getPaddingTop").invoke(layoutParams));
int right = fixDefault((Integer)layoutClass.getMethod("getPaddingRight").invoke(layoutParams));
int bottom = fixDefault((Integer)layoutClass.getMethod("getPaddingBottom").invoke(layoutParams));
if (left == 0 && top == 0 && right == 0 && bottom == 0) {
myPadding = Insets.NONE;
} else {
myPadding = new Insets(left, top, right, bottom);
}
}
catch (Throwable e) {
myPadding = Insets.NONE;
}
}
return myPadding;
}
public Insets getMargins(Component relativeTo) {
Insets margins = getMargins();
if (margins.isEmpty()) {
return margins;
}
return fromModel(relativeTo, margins);
}
public Insets getPadding(Component relativeTo) {
Insets padding = getPadding();
if (padding.isEmpty()) {
return padding;
}
return fromModel(relativeTo, padding);
}
private static int fixDefault(int value) {
return value == Integer.MIN_VALUE ? 0 : value;
}
private static final int WRAP_CONTENT = 0 << 30;
public boolean calculateWrapSize(@NotNull Dimension wrapSize, @Nullable Rectangle bounds) {
if (wrapSize.width == -1 || wrapSize.height == -1) {
try {
Object viewObject = myViewInfo.getViewObject();
Class<?> viewClass = viewObject.getClass();
viewClass.getMethod("forceLayout").invoke(viewObject);
viewClass.getMethod("measure", int.class, int.class).invoke(viewObject, WRAP_CONTENT, WRAP_CONTENT);
if (wrapSize.width == -1) {
wrapSize.width = (Integer)viewClass.getMethod("getMeasuredWidth").invoke(viewObject);
}
if (wrapSize.height == -1) {
wrapSize.height = (Integer)viewClass.getMethod("getMeasuredHeight").invoke(viewObject);
}
return true;
}
catch (Throwable e) {
if (bounds != null) {
if (wrapSize.width == -1) {
wrapSize.width = bounds.width;
}
if (wrapSize.height == -1) {
wrapSize.height = bounds.height;
}
}
}
}
return false;
}
@Nullable
public Dimension calculateWrapSize(EditableArea area) {
if (myViewInfo != null) {
Dimension dimension = new Dimension(-1, -1);
boolean measured = calculateWrapSize(dimension, null);
if (measured) {
return dimension;
}
}
RadComponent parent = getParent();
if (!(parent instanceof RadViewComponent)) {
return null;
}
XmlTag parentTag = ((RadViewComponent)parent).getTag();
if (parentTag != null) {
RenderTask task = AndroidDesignerUtils.createRenderTask(area);
if (task == null) {
return null;
}
ViewInfo viewInfo = task.measureChild(getTag(), new RenderTask.AttributeFilter() {
@Override
public String getAttribute(@NotNull XmlTag n, @Nullable String namespace, @NotNull String localName) {
if ((ATTR_LAYOUT_WIDTH.equals(localName) || ATTR_LAYOUT_HEIGHT.equals(localName)) && ANDROID_URI.equals(namespace)) {
return VALUE_WRAP_CONTENT;
} else if (ATTR_LAYOUT_WEIGHT.equals(localName) && ANDROID_URI.equals(namespace)) {
return ""; // unset
}
return null; // use default
}
});
if (viewInfo != null) {
return new Dimension(viewInfo.getRight() - viewInfo.getLeft(), viewInfo.getBottom() - viewInfo.getTop());
}
}
return null;
}
//////////////////////////////////////////////////////////////////////////////////////////
//
//
//
//////////////////////////////////////////////////////////////////////////////////////////
@NotNull
@Override
public List<RadComponent> getChildren() {
return myChildren;
}
@Override
public boolean isBackground() {
// In Android layouts there are two levels of parents; at the top level
// there is the "Device Screen", which is not deletable.
// Below that is the root layout, which we want to consider as the background,
// such that a marquee selection within the layout drag will select its children.
// This will also make select all operate on the children of the layout.
RadComponent parent = getParent();
if (parent != null) {
if (parent.getParent() == null && !parent.getMetaModel().isTag(VIEW_MERGE)) {
IncludeReference includeContext = parent.getClientProperty(ATTR_RENDER_IN);
if (includeContext != null && includeContext != IncludeReference.NONE) {
return false;
}
return true;
}
}
return parent == null;
}
@Override
public boolean canDelete() {
return !isBackground() && super.canDelete();
}
@Override
public void delete() throws Exception {
if (getParent() != null) {
removeFromParent();
}
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
myTag.delete();
}
});
}
@Override
public List<Property> getProperties() {
return myProperties;
}
public void setProperties(List<Property> properties) {
myProperties = properties;
}
@Override
public List<Property> getInplaceProperties() throws Exception {
List<Property> properties = super.getInplaceProperties();
Property idProperty = PropertyTable.findProperty(myProperties, "id");
if (idProperty != null) {
properties.add(idProperty);
}
return properties;
}
@Override
public void copyTo(Element parent) throws Exception {
// skip root
if (getParent() != null) {
Element component = new Element("component");
component.setAttribute("tag", myTag.getName());
XmlAttribute[] attributes = myTag.getAttributes();
if (attributes.length > 0) {
Element properties = new Element("properties");
component.addContent(properties);
Map<String, Element> namespaces = new HashMap<String, Element>();
for (XmlAttribute attribute : attributes) {
String namespace = attribute.getNamespacePrefix();
if (namespace.length() == 0) {
properties.setAttribute(attribute.getName(), attribute.getValue());
}
else {
Element element = namespaces.get(namespace);
if (element == null) {
element = new Element(namespace);
namespaces.put(namespace, element);
}
element.setAttribute(attribute.getLocalName(), attribute.getValue());
}
}
for (Element element : namespaces.values()) {
properties.addContent(element);
}
}
parent.addContent(component);
parent = component;
}
for (RadComponent child : myChildren) {
child.copyTo(parent);
}
}
@Override
public boolean isSameType(@NotNull RadComponent other) {
if (myTag != null) {
if (!(other instanceof RadViewComponent)) {
return false;
}
RadViewComponent otherView = (RadViewComponent)other;
if (otherView.myTag == null) {
return false;
}
return myTag.getName().equals(otherView.myTag.getName());
}
return super.isSameType(other);
}
@Override
public RadComponent morphingTo(MetaModel target) throws Exception {
return new ComponentMorphingTool(this, this, target, null).result();
}
/** Sets the palette item this component was initially created from */
public void setInitialPaletteItem(PaletteItem paletteItem) {
myPaletteItem = paletteItem;
}
/**
* The palette item this component was initially created from, if known.
* This will be null for widgets created through other means (pasting, typing code, etc)
* or after an IDE restart.
*/
@Nullable
public PaletteItem getInitialPaletteItem() {
return myPaletteItem;
}
/**
* Like {@link #toModel(java.awt.Component, java.awt.Rectangle)}, but
* also translates from pixels in the source, to device independent pixels
* in the model.
* <p>
* A lot of client code needs to compute model dp by doing arithmetic on
* bounds in the tool (which are scaled). By first computing to model
* pixels (with {@link #toModel(java.awt.Component, java.awt.Rectangle)},
* and <b>then</b> computing the dp from there, you introduce two levels
* of rounding.
* <p>
* This method performs both computations in a single go which reduces
* the amount of rounding error.
*/
public Rectangle toModelDp(int dpi, @NotNull Component source, @NotNull Rectangle rectangle) {
Component nativeComponent = getNativeComponent();
Rectangle bounds = nativeComponent == source
? rectangle : SwingUtilities.convertRectangle(source, rectangle, nativeComponent);
// To convert px to dpi, dp = px * 160 / dpi
double x = 160 * bounds.x;
double y = 160 * bounds.y;
double w = 160 * bounds.width;
double h = 160 * bounds.height;
if (nativeComponent != source && nativeComponent instanceof TransformedComponent) {
TransformedComponent transform = (TransformedComponent)nativeComponent;
x -= transform.getShiftX();
y -= transform.getShiftY();
double zoom = transform.getScale();
if (zoom != 1) {
x /= zoom;
y /= zoom;
w /= zoom;
h /= zoom;
}
}
@SuppressWarnings("UnnecessaryLocalVariable")
double dpiDouble = dpi;
return new Rectangle(
(int)(x / dpiDouble),
(int)(y / dpiDouble),
(int)(w / dpiDouble),
(int)(h / dpiDouble));
}
/**
* Like {@link #toModel(java.awt.Component, java.awt.Point)}, but
* also translates from pixels in the source, to device independent pixels
* in the model.
* <p>
* A lot of client code needs to compute model dp by doing arithmetic on
* bounds in the tool (which are scaled). By first computing to model
* pixels (with {@link #toModel(java.awt.Component, java.awt.Point)},
* and <b>then</b> computing the dp from there, you introduce two levels
* of rounding.
* <p>
* This method performs both computations in a single go which reduces
* the amount of rounding error.
*/
public Point toModelDp(int dpi, @NotNull Component source, @NotNull Point point) {
Component nativeComponent = getNativeComponent();
Point bounds = nativeComponent == source
? point : SwingUtilities.convertPoint(source, point, nativeComponent);
// To convert px to dpi, dp = px * 160 / dpi
double x = 160 * bounds.x;
double y = 160 * bounds.y;
if (nativeComponent != source && nativeComponent instanceof TransformedComponent) {
TransformedComponent transform = (TransformedComponent)nativeComponent;
x -= transform.getShiftX();
y -= transform.getShiftY();
double zoom = transform.getScale();
if (zoom != 1) {
x /= zoom;
y /= zoom;
}
}
@SuppressWarnings("UnnecessaryLocalVariable")
double dpiDouble = dpi;
return new Point(
(int)(x / dpiDouble),
(int)(y / dpiDouble));
}
/**
* Like {@link #toModel(java.awt.Component, java.awt.Dimension)}, but
* also translates from pixels in the source, to device independent pixels
* in the model.
* <p>
* A lot of client code needs to compute model dp by doing arithmetic on
* bounds in the tool (which are scaled). By first computing to model
* pixels (with {@link #toModel(java.awt.Component, java.awt.Dimension)},
* and <b>then</b> computing the dp from there, you introduce two levels
* of rounding.
* <p>
* This method performs both computations in a single go which reduces
* the amount of rounding error.
*/
public Dimension toModelDp(int dpi, @NotNull Component source, @NotNull Dimension size) {
Component nativeComponent = getNativeComponent();
size = new Dimension(size);
// To convert px to dpi, dp = px * 160 / dpi
double w = 160 * size.width;
double h = 160 * size.height;
if (nativeComponent != source && nativeComponent instanceof ScalableComponent) {
ScalableComponent scalableComponent = (ScalableComponent)nativeComponent;
double zoom = scalableComponent.getScale();
if (zoom != 1) {
w /= zoom;
h /= zoom;
}
}
@SuppressWarnings("UnnecessaryLocalVariable")
double dpiDouble = dpi;
return new Dimension(
(int)(w / dpiDouble),
(int)(h / dpiDouble));
}
public Insets fromModel(@NotNull Component target, @NotNull Insets insets) {
if (insets.isEmpty()) {
return insets;
}
Component nativeComponent = getNativeComponent();
if (target != nativeComponent && nativeComponent instanceof ScalableComponent) {
ScalableComponent scalableComponent = (ScalableComponent)nativeComponent;
double zoom = scalableComponent.getScale();
if (zoom != 1) {
return new Insets((int)(insets.left * zoom), (int)(insets.top * zoom), (int)(insets.right * zoom),
(int)(insets.bottom * zoom));
}
}
return insets;
}
public Insets toModel(@NotNull Component source, @NotNull Insets insets) {
if (insets.isEmpty()) {
return insets;
}
Component nativeComponent = getNativeComponent();
if (source != nativeComponent && nativeComponent instanceof ScalableComponent) {
ScalableComponent scalableComponent = (ScalableComponent)nativeComponent;
double zoom = scalableComponent.getScale();
if (zoom != 1) {
return new Insets((int)(insets.left / zoom), (int)(insets.top / zoom), (int)(insets.right / zoom),
(int)(insets.bottom / zoom));
}
}
return insets;
}
/**
* Returns the bounds of this {@linkplain RadViewComponent} in the model, minus any
* padding (if any).
* <p/>
* Caller should <b>not</b> modify this rectangle.
*
* @return the padded bounds of this {@linkplain RadViewComponent} in the model coordinate system
* (e.g. unaffected by a view zoom for example)
*/
public Rectangle getPaddedBounds() {
Rectangle bounds = getBounds();
Insets padding = getPadding();
if (padding == Insets.NONE) {
return bounds;
}
return new Rectangle(bounds.x + padding.left,
bounds.y + padding.top,
Math.max(0, bounds.width - padding.left - padding.right),
Math.max(0, bounds.height - padding.top - padding.bottom));
}
/**
* Like {@link #getBounds(java.awt.Component)}, but rather than applying to the
* bounds of the view, it applies to the padded bounds (e.g. with insets applied).
*
* @param relativeTo the component whose coordinate system the model bounds should
* be shifted and scaled into
* @return the padded bounds of this {@linkplain RadComponent} in the given coordinate system
*/
public Rectangle getPaddedBounds(Component relativeTo) {
return fromModel(relativeTo, getPaddedBounds());
}
/** Extracts the {@link RadViewComponent} elements from the list */
@NotNull
@SuppressWarnings("unchecked")
public static List<RadViewComponent> getViewComponents(@NotNull List<? extends RadComponent> components) {
for (RadComponent component : components) {
if (!(component instanceof RadViewComponent)) {
List<RadViewComponent> newList = new ArrayList<RadViewComponent>(components.size() - 1);
for (RadComponent c : components) {
if (c instanceof RadViewComponent) {
newList.add((RadViewComponent)c);
}
}
return newList;
}
}
return (List<RadViewComponent>)(List)components;
}
/** Adds in actions for this component into the given popup context menu
* Return true if anything was added.
*/
public boolean addPopupActions(@NotNull AndroidDesignerEditorPanel designer,
@NotNull DefaultActionGroup beforeGroup,
@NotNull DefaultActionGroup afterGroup,
@Nullable JComponent shortcuts,
@NotNull List<RadComponent> selection) {
return false;
}
// Coordinate transformations.
// These are like the implementations in RadVisualComponent (the parent class), except
// that instead of looking for a ScalableComponent it checks for its sub-interface,
// TransformedComponent, which adds in translation as well as part of the transform.
@Override
public Rectangle fromModel(@NotNull Component target, @NotNull Rectangle bounds) {
Component nativeComponent = getNativeComponent();
if (target != nativeComponent && nativeComponent instanceof TransformedComponent) {
TransformedComponent transform = (TransformedComponent)nativeComponent;
int shiftX = transform.getShiftX();
int shiftY = transform.getShiftY();
double zoom = transform.getScale();
if (zoom != 1 || shiftX != 0 || shiftY != 0) {
bounds = new Rectangle(bounds);
bounds.x *= zoom;
bounds.y *= zoom;
bounds.width *= zoom;
bounds.height *= zoom;
bounds.x += shiftX;
bounds.y += shiftY;
}
}
return nativeComponent == target
? new Rectangle(bounds) :
SwingUtilities.convertRectangle(nativeComponent, bounds, target);
}
@Override
public Rectangle toModel(@NotNull Component source, @NotNull Rectangle rectangle) {
Component nativeComponent = getNativeComponent();
Rectangle bounds = nativeComponent == source
? new Rectangle(rectangle) : SwingUtilities.convertRectangle(source, rectangle, nativeComponent);
if (nativeComponent != source && nativeComponent instanceof TransformedComponent) {
TransformedComponent transform = (TransformedComponent)nativeComponent;
bounds.x -= transform.getShiftX();
bounds.y -= transform.getShiftY();
double zoom = transform.getScale();
if (zoom != 1) {
bounds = new Rectangle(bounds);
bounds.x /= zoom;
bounds.y /= zoom;
bounds.width /= zoom;
bounds.height /= zoom;
}
}
return bounds;
}
@Override
public Point fromModel(@NotNull Component target, @NotNull Point point) {
Component nativeComponent = getNativeComponent();
if (target != nativeComponent && nativeComponent instanceof TransformedComponent) {
TransformedComponent transform = (TransformedComponent)nativeComponent;
int shiftX = transform.getShiftX();
int shiftY = transform.getShiftY();
double zoom = transform.getScale();
if (zoom != 1 || shiftX != 0 || shiftY != 0) {
point = new Point(point);
point.x *= zoom;
point.y *= zoom;
point.x += shiftX;
point.y += shiftY;
}
}
return nativeComponent == target
? new Point(point) :
SwingUtilities.convertPoint(nativeComponent, point, target);
}
@Override
public Point toModel(@NotNull Component source, @NotNull Point point) {
Component nativeComponent = getNativeComponent();
Point p = nativeComponent == source
? new Point(point) : SwingUtilities.convertPoint(source, point, nativeComponent);
if (nativeComponent != source && nativeComponent instanceof TransformedComponent) {
TransformedComponent transform = (TransformedComponent)nativeComponent;
p.x -= transform.getShiftX();
p.y -= transform.getShiftY();
double zoom = transform.getScale();
if (zoom != 1) {
p = new Point(p);
p.x /= zoom;
p.y /= zoom;
}
}
return p;
}
@Override
public Point convertPoint(Component relativeFrom, int x, int y) {
Component nativeComponent = getNativeComponent();
Point p = nativeComponent == relativeFrom ? new Point(x, y) : SwingUtilities.convertPoint(relativeFrom, x, y, nativeComponent);
if (nativeComponent != relativeFrom && nativeComponent instanceof TransformedComponent) {
TransformedComponent transform = (TransformedComponent)nativeComponent;
p.x -= transform.getShiftX();
p.y -= transform.getShiftY();
double zoom = transform.getScale();
if (zoom != 1) {
p.x /= zoom;
p.y /= zoom;
}
}
return p;
}
}