/* * 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.RenderService; 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; import static com.intellij.android.designer.model.RadModelBuilder.ROOT_NODE_TAG; /** * @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) { IdManager idManager = IdManager.get(this); if (idManager != null) { id = idManager.createId(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) { RenderService service = AndroidDesignerUtils.getRenderService(area); if (service == null) { return null; } ViewInfo viewInfo = service.measureChild(getTag(), new RenderService.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 void delete() throws Exception { IdManager idManager = IdManager.get(this); if (idManager != null) { idManager.removeComponent(this, true); } 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; } }