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 
17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
18 
19 import com.android.annotations.NonNull;
20 import com.android.annotations.Nullable;
21 import com.android.ide.common.api.IDragElement;
22 import com.android.ide.common.api.INode;
23 import com.android.ide.common.api.Rect;
24 
25 import java.util.ArrayList;
26 import 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  */
37 public 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      */
SimpleElement(String fqcn, String parentFqcn, Rect bounds, Rect parentBounds)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
getFqcn()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
getBounds()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
getParentFqcn()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
getParentBounds()105     public @NonNull Rect getParentBounds() {
106         return mParentBounds;
107     }
108 
109     @Override
getAttributes()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
getAttribute(@ullable String uri, @NonNull String localName)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
getInnerElements()129     public @NonNull IDragElement[] getInnerElements() {
130         if (mCachedElements == null) {
131             mCachedElements = mElements.toArray(new IDragElement[mElements.size()]);
132         }
133         return mCachedElements;
134     }
135 
addAttribute(SimpleAttribute attr)136     public void addAttribute(SimpleAttribute attr) {
137         mCachedAttributes = null;
138         mAttributes.add(attr);
139     }
140 
addInnerElement(SimpleElement e)141     public void addInnerElement(SimpleElement e) {
142         mCachedElements = null;
143         mElements.add(e);
144     }
145 
146     @Override
isSame(@onNull INode node)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 
setSelectionItem(@ullable SelectionItem selectionItem)155     void setSelectionItem(@Nullable SelectionItem selectionItem) {
156         mSelectionItem = selectionItem;
157     }
158 
159     @Nullable
getSelectionItem()160     SelectionItem getSelectionItem() {
161         return mSelectionItem;
162     }
163 
164     @Nullable
findPrimary(SimpleElement[] elements, SelectionItem primary)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
toString()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. */
parseString(String value)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      */
parseLines(String[] lines, int[] inOutIndex)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
equals(Object obj)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
hashCode()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