1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.eclipse.org/org/documents/epl-v10.php
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
18
19import com.android.annotations.NonNull;
20import com.android.annotations.Nullable;
21import com.android.ide.common.api.IDragElement;
22import com.android.ide.common.api.INode;
23import com.android.ide.common.api.Rect;
24
25import java.util.ArrayList;
26import java.util.List;
27
28/**
29 * Represents an XML element with a name, attributes and inner elements.
30 * <p/>
31 * The semantic of the element name is to be a fully qualified class name of a View to inflate.
32 * The element name is not expected to have a name space.
33 * <p/>
34 * For a more detailed explanation of the purpose of this class,
35 * please see {@link SimpleXmlTransfer}.
36 */
37public class SimpleElement implements IDragElement {
38
39    /** Version number of the internal serialized string format. */
40    private static final String FORMAT_VERSION = "3";
41
42    private final String mFqcn;
43    private final String mParentFqcn;
44    private final Rect mBounds;
45    private final Rect mParentBounds;
46    private final List<IDragAttribute> mAttributes = new ArrayList<IDragAttribute>();
47    private final List<IDragElement> mElements = new ArrayList<IDragElement>();
48
49    private IDragAttribute[] mCachedAttributes = null;
50    private IDragElement[] mCachedElements = null;
51    private SelectionItem mSelectionItem;
52
53    /**
54     * Creates a new {@link SimpleElement} with the specified element name.
55     *
56     * @param fqcn A fully qualified class name of a View to inflate, e.g.
57     *             "android.view.Button". Must not be null nor empty.
58     * @param parentFqcn The fully qualified class name of the parent of this element.
59     *                   Can be null but not empty.
60     * @param bounds The canvas bounds of the originating canvas node of the element.
61     *               If null, a non-null invalid rectangle will be assigned.
62     * @param parentBounds The canvas bounds of the parent of this element. Can be null.
63     */
64    public SimpleElement(String fqcn, String parentFqcn, Rect bounds, Rect parentBounds) {
65        mFqcn = fqcn;
66        mParentFqcn = parentFqcn;
67        mBounds = bounds == null ? new Rect() : bounds.copy();
68        mParentBounds = parentBounds == null ? new Rect() : parentBounds.copy();
69    }
70
71    /**
72     * Returns the element name, which must match a fully qualified class name of
73     * a View to inflate.
74     */
75    @Override
76    public @NonNull String getFqcn() {
77        return mFqcn;
78    }
79
80    /**
81     * Returns the bounds of the element's node, if it originated from an existing
82     * canvas. The rectangle is invalid and non-null when the element originated
83     * from the object palette (unless it successfully rendered a preview)
84     */
85    @Override
86    public @NonNull Rect getBounds() {
87        return mBounds;
88    }
89
90    /**
91     * Returns the fully qualified class name of the parent, if the element originated
92     * from an existing canvas. Returns null if the element has no parent, such as a top
93     * level element or an element originating from the object palette.
94     */
95    @Override
96    public String getParentFqcn() {
97        return mParentFqcn;
98    }
99
100    /**
101     * Returns the bounds of the element's parent, absolute for the canvas, or null if there
102     * is no suitable parent. This is null when {@link #getParentFqcn()} is null.
103     */
104    @Override
105    public @NonNull Rect getParentBounds() {
106        return mParentBounds;
107    }
108
109    @Override
110    public @NonNull IDragAttribute[] getAttributes() {
111        if (mCachedAttributes == null) {
112            mCachedAttributes = mAttributes.toArray(new IDragAttribute[mAttributes.size()]);
113        }
114        return mCachedAttributes;
115    }
116
117    @Override
118    public IDragAttribute getAttribute(@Nullable String uri, @NonNull String localName) {
119        for (IDragAttribute attr : mAttributes) {
120            if (attr.getUri().equals(uri) && attr.getName().equals(localName)) {
121                return attr;
122            }
123        }
124
125        return null;
126    }
127
128    @Override
129    public @NonNull IDragElement[] getInnerElements() {
130        if (mCachedElements == null) {
131            mCachedElements = mElements.toArray(new IDragElement[mElements.size()]);
132        }
133        return mCachedElements;
134    }
135
136    public void addAttribute(SimpleAttribute attr) {
137        mCachedAttributes = null;
138        mAttributes.add(attr);
139    }
140
141    public void addInnerElement(SimpleElement e) {
142        mCachedElements = null;
143        mElements.add(e);
144    }
145
146    @Override
147    public boolean isSame(@NonNull INode node) {
148        if (mSelectionItem != null) {
149            return node == mSelectionItem.getNode();
150        } else {
151            return node.getBounds().equals(mBounds);
152        }
153    }
154
155    void setSelectionItem(@Nullable SelectionItem selectionItem) {
156        mSelectionItem = selectionItem;
157    }
158
159    @Nullable
160    SelectionItem getSelectionItem() {
161        return mSelectionItem;
162    }
163
164    @Nullable
165    static SimpleElement findPrimary(SimpleElement[] elements, SelectionItem primary) {
166        if (elements == null || elements.length == 0) {
167            return null;
168        }
169
170        if (elements.length == 1 || primary == null) {
171            return elements[0];
172        }
173
174        for (SimpleElement element : elements) {
175            if (element.getSelectionItem() == primary) {
176                return element;
177            }
178        }
179
180        return elements[0];
181    }
182
183    // reader and writer methods
184
185    @Override
186    public String toString() {
187        StringBuilder sb = new StringBuilder();
188        sb.append("{V=").append(FORMAT_VERSION);
189        sb.append(",N=").append(mFqcn);
190        if (mParentFqcn != null) {
191            sb.append(",P=").append(mParentFqcn);
192        }
193        if (mBounds != null && mBounds.isValid()) {
194            sb.append(String.format(",R=%d %d %d %d", mBounds.x, mBounds.y, mBounds.w, mBounds.h));
195        }
196        if (mParentBounds != null && mParentBounds.isValid()) {
197            sb.append(String.format(",Q=%d %d %d %d",
198                    mParentBounds.x, mParentBounds.y, mParentBounds.w, mParentBounds.h));
199        }
200        sb.append('\n');
201        for (IDragAttribute a : mAttributes) {
202            sb.append(a.toString());
203        }
204        for (IDragElement e : mElements) {
205            sb.append(e.toString());
206        }
207        sb.append("}\n"); //$NON-NLS-1$
208        return sb.toString();
209    }
210
211    /** Parses a string containing one or more elements. */
212    static SimpleElement[] parseString(String value) {
213        ArrayList<SimpleElement> elements = new ArrayList<SimpleElement>();
214        String[] lines = value.split("\n");
215        int[] index = new int[] { 0 };
216        SimpleElement element = null;
217        while ((element = parseLines(lines, index)) != null) {
218            elements.add(element);
219        }
220        return elements.toArray(new SimpleElement[elements.size()]);
221    }
222
223    /**
224     * Parses one element from the input lines array, starting at the inOutIndex
225     * and updating the inOutIndex to match the next unread line on output.
226     */
227    private static SimpleElement parseLines(String[] lines, int[] inOutIndex) {
228        SimpleElement e = null;
229        int index = inOutIndex[0];
230        while (index < lines.length) {
231            String line = lines[index++];
232            String s = line.trim();
233            if (s.startsWith("{")) {                                //$NON-NLS-1$
234                if (e == null) {
235                    // This is the element's header, it should have
236                    // the format "key=value,key=value,..."
237                    String version = null;
238                    String fqcn = null;
239                    String parent = null;
240                    Rect bounds = null;
241                    Rect pbounds = null;
242
243                    for (String s2 : s.substring(1).split(",")) {   //$NON-NLS-1$
244                        int pos = s2.indexOf('=');
245                        if (pos <= 0 || pos == s2.length() - 1) {
246                            continue;
247                        }
248                        String key = s2.substring(0, pos).trim();
249                        String value = s2.substring(pos + 1).trim();
250
251                        if (key.equals("V")) {                      //$NON-NLS-1$
252                            version = value;
253                            if (!value.equals(FORMAT_VERSION)) {
254                                // Wrong format version. Don't even try to process anything
255                                // else and just give up everything.
256                                inOutIndex[0] = index;
257                                return null;
258                            }
259
260                        } else if (key.equals("N")) {               //$NON-NLS-1$
261                            fqcn = value;
262
263                        } else if (key.equals("P")) {               //$NON-NLS-1$
264                            parent = value;
265
266                        } else if (key.equals("R") || key.equals("Q")) { //$NON-NLS-1$ //$NON-NLS-2$
267                            // Parse the canvas bounds
268                            String[] sb = value.split(" +");        //$NON-NLS-1$
269                            if (sb != null && sb.length == 4) {
270                                Rect r = null;
271                                try {
272                                    r = new Rect();
273                                    r.x = Integer.parseInt(sb[0]);
274                                    r.y = Integer.parseInt(sb[1]);
275                                    r.w = Integer.parseInt(sb[2]);
276                                    r.h = Integer.parseInt(sb[3]);
277
278                                    if (key.equals("R")) {
279                                        bounds = r;
280                                    } else {
281                                        pbounds = r;
282                                    }
283                                } catch (NumberFormatException ignore) {
284                                }
285                            }
286                        }
287                    }
288
289                    // We need at least a valid name to recreate an element
290                    if (version != null && fqcn != null && fqcn.length() > 0) {
291                        e = new SimpleElement(fqcn, parent, bounds, pbounds);
292                    }
293                } else {
294                    // This is an inner element... need to parse the { line again.
295                    inOutIndex[0] = index - 1;
296                    SimpleElement e2 = SimpleElement.parseLines(lines, inOutIndex);
297                    if (e2 != null) {
298                        e.addInnerElement(e2);
299                    }
300                    index = inOutIndex[0];
301                }
302
303            } else if (e != null && s.startsWith("@")) {    //$NON-NLS-1$
304                SimpleAttribute a = SimpleAttribute.parseString(line);
305                if (a != null) {
306                    e.addAttribute(a);
307                }
308
309            } else if (e != null && s.startsWith("}")) {     //$NON-NLS-1$
310                // We're done with this element
311                inOutIndex[0] = index;
312                return e;
313            }
314        }
315        inOutIndex[0] = index;
316        return null;
317    }
318
319    @Override
320    public boolean equals(Object obj) {
321        if (obj instanceof SimpleElement) {
322            SimpleElement se = (SimpleElement) obj;
323
324            // Bounds and parentFqcn must be null on both sides or equal.
325            if ((mBounds == null && se.mBounds != null) ||
326                    (mBounds != null && !mBounds.equals(se.mBounds))) {
327                return false;
328            }
329            if ((mParentFqcn == null && se.mParentFqcn != null) ||
330                    (mParentFqcn != null && !mParentFqcn.equals(se.mParentFqcn))) {
331                return false;
332            }
333            if ((mParentBounds == null && se.mParentBounds != null) ||
334                    (mParentBounds != null && !mParentBounds.equals(se.mParentBounds))) {
335                return false;
336            }
337
338            return mFqcn.equals(se.mFqcn) &&
339                    mAttributes.size() == se.mAttributes.size() &&
340                    mElements.size() == se.mElements.size() &&
341                    mAttributes.equals(se.mAttributes) &&
342                    mElements.equals(se.mElements);
343        }
344        return false;
345    }
346
347    @Override
348    public int hashCode() {
349        long c = mFqcn.hashCode();
350        // uses the formula defined in java.util.List.hashCode()
351        c = 31*c + mAttributes.hashCode();
352        c = 31*c + mElements.hashCode();
353        if (mParentFqcn != null) {
354            c = 31*c + mParentFqcn.hashCode();
355        }
356        if (mBounds != null && mBounds.isValid()) {
357            c = 31*c + mBounds.hashCode();
358        }
359        if (mParentBounds != null && mParentBounds.isValid()) {
360            c = 31*c + mParentBounds.hashCode();
361        }
362
363        if (c > 0x0FFFFFFFFL) {
364            // wrap any overflow
365            c = c ^ (c >> 32);
366        }
367        return (int)(c & 0x0FFFFFFFFL);
368    }
369}
370
371