/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php * * 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.ide.eclipse.adt.internal.editors.layout.gle2; import com.android.ide.common.api.IDragElement; import com.android.ide.common.api.Rect; import java.util.ArrayList; /** * Represents an XML element with a name, attributes and inner elements. * <p/> * The semantic of the element name is to be a fully qualified class name of a View to inflate. * The element name is not expected to have a name space. * <p/> * For a more detailed explanation of the purpose of this class, * please see {@link SimpleXmlTransfer}. */ public class SimpleElement implements IDragElement { /** Version number of the internal serialized string format. */ private static final String FORMAT_VERSION = "3"; private final String mFqcn; private final String mParentFqcn; private final Rect mBounds; private final Rect mParentBounds; private final ArrayList<IDragAttribute> mAttributes = new ArrayList<IDragAttribute>(); private final ArrayList<IDragElement> mElements = new ArrayList<IDragElement>(); private IDragAttribute[] mCachedAttributes = null; private IDragElement[] mCachedElements = null; /** * Creates a new {@link SimpleElement} with the specified element name. * * @param fqcn A fully qualified class name of a View to inflate, e.g. * "android.view.Button". Must not be null nor empty. * @param parentFqcn The fully qualified class name of the parent of this element. * Can be null but not empty. * @param bounds The canvas bounds of the originating canvas node of the element. * If null, a non-null invalid rectangle will be assigned. * @param parentBounds The canvas bounds of the parent of this element. Can be null. */ public SimpleElement(String fqcn, String parentFqcn, Rect bounds, Rect parentBounds) { mFqcn = fqcn; mParentFqcn = parentFqcn; mBounds = bounds == null ? new Rect() : bounds.copy(); mParentBounds = parentBounds == null ? new Rect() : parentBounds.copy(); } /** * Returns the element name, which must match a fully qualified class name of * a View to inflate. */ public String getFqcn() { return mFqcn; } /** * Returns the bounds of the element's node, if it originated from an existing * canvas. The rectangle is invalid and non-null when the element originated * from the object palette (unless it successfully rendered a preview) */ public Rect getBounds() { return mBounds; } /** * Returns the fully qualified class name of the parent, if the element originated * from an existing canvas. Returns null if the element has no parent, such as a top * level element or an element originating from the object palette. */ public String getParentFqcn() { return mParentFqcn; } /** * Returns the bounds of the element's parent, absolute for the canvas, or null if there * is no suitable parent. This is null when {@link #getParentFqcn()} is null. */ public Rect getParentBounds() { return mParentBounds; } public IDragAttribute[] getAttributes() { if (mCachedAttributes == null) { mCachedAttributes = mAttributes.toArray(new IDragAttribute[mAttributes.size()]); } return mCachedAttributes; } public IDragAttribute getAttribute(String uri, String localName) { for (IDragAttribute attr : mAttributes) { if (attr.getUri().equals(uri) && attr.getName().equals(localName)) { return attr; } } return null; } public IDragElement[] getInnerElements() { if (mCachedElements == null) { mCachedElements = mElements.toArray(new IDragElement[mElements.size()]); } return mCachedElements; } public void addAttribute(SimpleAttribute attr) { mCachedAttributes = null; mAttributes.add(attr); } public void addInnerElement(SimpleElement e) { mCachedElements = null; mElements.add(e); } // reader and writer methods @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("{V=").append(FORMAT_VERSION); sb.append(",N=").append(mFqcn); if (mParentFqcn != null) { sb.append(",P=").append(mParentFqcn); } if (mBounds != null && mBounds.isValid()) { sb.append(String.format(",R=%d %d %d %d", mBounds.x, mBounds.y, mBounds.w, mBounds.h)); } if (mParentBounds != null && mParentBounds.isValid()) { sb.append(String.format(",Q=%d %d %d %d", mParentBounds.x, mParentBounds.y, mParentBounds.w, mParentBounds.h)); } sb.append('\n'); for (IDragAttribute a : mAttributes) { sb.append(a.toString()); } for (IDragElement e : mElements) { sb.append(e.toString()); } sb.append("}\n"); //$NON-NLS-1$ return sb.toString(); } /** Parses a string containing one or more elements. */ static SimpleElement[] parseString(String value) { ArrayList<SimpleElement> elements = new ArrayList<SimpleElement>(); String[] lines = value.split("\n"); int[] index = new int[] { 0 }; SimpleElement element = null; while ((element = parseLines(lines, index)) != null) { elements.add(element); } return elements.toArray(new SimpleElement[elements.size()]); } /** * Parses one element from the input lines array, starting at the inOutIndex * and updating the inOutIndex to match the next unread line on output. */ private static SimpleElement parseLines(String[] lines, int[] inOutIndex) { SimpleElement e = null; int index = inOutIndex[0]; while (index < lines.length) { String line = lines[index++]; String s = line.trim(); if (s.startsWith("{")) { //$NON-NLS-1$ if (e == null) { // This is the element's header, it should have // the format "key=value,key=value,..." String version = null; String fqcn = null; String parent = null; Rect bounds = null; Rect pbounds = null; for (String s2 : s.substring(1).split(",")) { //$NON-NLS-1$ int pos = s2.indexOf('='); if (pos <= 0 || pos == s2.length() - 1) { continue; } String key = s2.substring(0, pos).trim(); String value = s2.substring(pos + 1).trim(); if (key.equals("V")) { //$NON-NLS-1$ version = value; if (!value.equals(FORMAT_VERSION)) { // Wrong format version. Don't even try to process anything // else and just give up everything. inOutIndex[0] = index; return null; } } else if (key.equals("N")) { //$NON-NLS-1$ fqcn = value; } else if (key.equals("P")) { //$NON-NLS-1$ parent = value; } else if (key.equals("R") || key.equals("Q")) { //$NON-NLS-1$ //$NON-NLS-2$ // Parse the canvas bounds String[] sb = value.split(" +"); //$NON-NLS-1$ if (sb != null && sb.length == 4) { Rect r = null; try { r = new Rect(); r.x = Integer.parseInt(sb[0]); r.y = Integer.parseInt(sb[1]); r.w = Integer.parseInt(sb[2]); r.h = Integer.parseInt(sb[3]); if (key.equals("R")) { bounds = r; } else { pbounds = r; } } catch (NumberFormatException ignore) { } } } } // We need at least a valid name to recreate an element if (version != null && fqcn != null && fqcn.length() > 0) { e = new SimpleElement(fqcn, parent, bounds, pbounds); } } else { // This is an inner element... need to parse the { line again. inOutIndex[0] = index - 1; SimpleElement e2 = SimpleElement.parseLines(lines, inOutIndex); if (e2 != null) { e.addInnerElement(e2); } index = inOutIndex[0]; } } else if (e != null && s.startsWith("@")) { //$NON-NLS-1$ SimpleAttribute a = SimpleAttribute.parseString(line); if (a != null) { e.addAttribute(a); } } else if (e != null && s.startsWith("}")) { //$NON-NLS-1$ // We're done with this element inOutIndex[0] = index; return e; } } inOutIndex[0] = index; return null; } @Override public boolean equals(Object obj) { if (obj instanceof SimpleElement) { SimpleElement se = (SimpleElement) obj; // Bounds and parentFqcn must be null on both sides or equal. if ((mBounds == null && se.mBounds != null) || (mBounds != null && !mBounds.equals(se.mBounds))) { return false; } if ((mParentFqcn == null && se.mParentFqcn != null) || (mParentFqcn != null && !mParentFqcn.equals(se.mParentFqcn))) { return false; } if ((mParentBounds == null && se.mParentBounds != null) || (mParentBounds != null && !mParentBounds.equals(se.mParentBounds))) { return false; } return mFqcn.equals(se.mFqcn) && mAttributes.size() == se.mAttributes.size() && mElements.size() == se.mElements.size() && mAttributes.equals(se.mAttributes) && mElements.equals(mElements); } return false; } @Override public int hashCode() { long c = mFqcn.hashCode(); // uses the formula defined in java.util.List.hashCode() c = 31*c + mAttributes.hashCode(); c = 31*c + mElements.hashCode(); if (mParentFqcn != null) { c = 31*c + mParentFqcn.hashCode(); } if (mBounds != null && mBounds.isValid()) { c = 31*c + mBounds.hashCode(); } if (mParentBounds != null && mParentBounds.isValid()) { c = 31*c + mParentBounds.hashCode(); } if (c > 0x0FFFFFFFFL) { // wrap any overflow c = c ^ (c >> 32); } return (int)(c & 0x0FFFFFFFFL); } }