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