blob: 9acc8c25e6cb9faeed803f5a688d4187c912fffa [file] [log] [blame]
/*
* 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);
}
}