| /* |
| * 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.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.ide.common.api.IDragElement; |
| import com.android.ide.common.api.INode; |
| import com.android.ide.common.api.Rect; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * 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 List<IDragAttribute> mAttributes = new ArrayList<IDragAttribute>(); |
| private final List<IDragElement> mElements = new ArrayList<IDragElement>(); |
| |
| private IDragAttribute[] mCachedAttributes = null; |
| private IDragElement[] mCachedElements = null; |
| private SelectionItem mSelectionItem; |
| |
| /** |
| * 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. |
| */ |
| @Override |
| public @NonNull 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) |
| */ |
| @Override |
| public @NonNull 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. |
| */ |
| @Override |
| 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. |
| */ |
| @Override |
| public @NonNull Rect getParentBounds() { |
| return mParentBounds; |
| } |
| |
| @Override |
| public @NonNull IDragAttribute[] getAttributes() { |
| if (mCachedAttributes == null) { |
| mCachedAttributes = mAttributes.toArray(new IDragAttribute[mAttributes.size()]); |
| } |
| return mCachedAttributes; |
| } |
| |
| @Override |
| public IDragAttribute getAttribute(@Nullable String uri, @NonNull String localName) { |
| for (IDragAttribute attr : mAttributes) { |
| if (attr.getUri().equals(uri) && attr.getName().equals(localName)) { |
| return attr; |
| } |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public @NonNull 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); |
| } |
| |
| @Override |
| public boolean isSame(@NonNull INode node) { |
| if (mSelectionItem != null) { |
| return node == mSelectionItem.getNode(); |
| } else { |
| return node.getBounds().equals(mBounds); |
| } |
| } |
| |
| void setSelectionItem(@Nullable SelectionItem selectionItem) { |
| mSelectionItem = selectionItem; |
| } |
| |
| @Nullable |
| SelectionItem getSelectionItem() { |
| return mSelectionItem; |
| } |
| |
| @Nullable |
| static SimpleElement findPrimary(SimpleElement[] elements, SelectionItem primary) { |
| if (elements == null || elements.length == 0) { |
| return null; |
| } |
| |
| if (elements.length == 1 || primary == null) { |
| return elements[0]; |
| } |
| |
| for (SimpleElement element : elements) { |
| if (element.getSelectionItem() == primary) { |
| return element; |
| } |
| } |
| |
| return elements[0]; |
| } |
| |
| // 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(se.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); |
| } |
| } |
| |