aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java
blob: ed447c6340e0c697645ccfd4c6cdb4697d909f9c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
/*
 * Copyright (C) 2007 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.uimodel;

import static com.android.SdkConstants.ANDROID_PKG_PREFIX;
import static com.android.SdkConstants.ANDROID_SUPPORT_PKG_PREFIX;
import static com.android.SdkConstants.ATTR_CLASS;
import static com.android.SdkConstants.ID_PREFIX;
import static com.android.SdkConstants.NEW_ID_PREFIX;

import com.android.SdkConstants;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.api.IAttributeInfo.Format;
import com.android.ide.common.resources.platform.AttributeInfo;
import com.android.ide.common.xml.XmlAttributeSortOrder;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor.Mandatory;
import com.android.ide.eclipse.adt.internal.editors.descriptors.IUnknownDescriptorProvider;
import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
import com.android.ide.eclipse.adt.internal.editors.otherxml.descriptors.OtherXmlDescriptors;
import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener.UiUpdateState;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
import com.android.utils.SdkUtils;
import com.android.utils.XmlUtils;

import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.ui.views.properties.IPropertyDescriptor;
import org.eclipse.ui.views.properties.IPropertySource;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.xml.core.internal.document.ElementImpl;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.Text;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * Represents an XML node that can be modified by the user interface in the XML editor.
 * <p/>
 * Each tree viewer used in the application page's parts needs to keep a model representing
 * each underlying node in the tree. This interface represents the base type for such a node.
 * <p/>
 * Each node acts as an intermediary model between the actual XML model (the real data support)
 * and the tree viewers or the corresponding page parts.
 * <p/>
 * Element nodes don't contain data per se. Their data is contained in their attributes
 * as well as their children's attributes, see {@link UiAttributeNode}.
 * <p/>
 * The structure of a given {@link UiElementNode} is declared by a corresponding
 * {@link ElementDescriptor}.
 * <p/>
 * The class implements {@link IPropertySource}, in order to fill the Eclipse property tab when
 * an element is selected. The {@link AttributeDescriptor} are used property descriptors.
 */
@SuppressWarnings("restriction") // XML model
public class UiElementNode implements IPropertySource {

    /** List of prefixes removed from android:id strings when creating short descriptions. */
    private static String[] ID_PREFIXES = {
        "@android:id/", //$NON-NLS-1$
        NEW_ID_PREFIX, ID_PREFIX, "@+", "@" }; //$NON-NLS-1$ //$NON-NLS-2$

    /** The element descriptor for the node. Always present, never null. */
    private ElementDescriptor mDescriptor;
    /** The parent element node in the UI model. It is null for a root element or until
     *  the node is attached to its parent. */
    private UiElementNode mUiParent;
    /** The {@link AndroidXmlEditor} handling the UI hierarchy. This is defined only for the
     *  root node. All children have the value set to null and query their parent. */
    private AndroidXmlEditor mEditor;
    /** The XML {@link Document} model that is being mirror by the UI model. This is defined
     *  only for the root node. All children have the value set to null and query their parent. */
    private Document mXmlDocument;
    /** The XML {@link Node} mirror by this UI node. This can be null for mandatory UI node which
     *  have no corresponding XML node or for new UI nodes before their XML node is set. */
    private Node mXmlNode;
    /** The list of all UI children nodes. Can be empty but never null. There's one UI children
     *  node per existing XML children node. */
    private ArrayList<UiElementNode> mUiChildren;
    /** The list of <em>all</em> UI attributes, as declared in the {@link ElementDescriptor}.
     *  The list is always defined and never null. Unlike the UiElementNode children list, this
     *  is always defined, even for attributes that do not exist in the XML model - that's because
     *  "missing" attributes in the XML model simply mean a default value is used. Also note that
     *  the underlying collection is a map, so order is not respected. To get the desired attribute
     *  order, iterate through the {@link ElementDescriptor}'s attribute list. */
    private HashMap<AttributeDescriptor, UiAttributeNode> mUiAttributes;
    private HashSet<UiAttributeNode> mUnknownUiAttributes;
    /** A read-only view of the UI children node collection. */
    private List<UiElementNode> mReadOnlyUiChildren;
    /** A read-only view of the UI attributes collection. */
    private Collection<UiAttributeNode> mCachedAllUiAttributes;
    /** A map of hidden attribute descriptors. Key is the XML name. */
    private Map<String, AttributeDescriptor> mCachedHiddenAttributes;
    /** An optional list of {@link IUiUpdateListener}. Most element nodes will not have any
     *  listeners attached, so the list is only created on demand and can be null. */
    private List<IUiUpdateListener> mUiUpdateListeners;
    /** A provider that knows how to create {@link ElementDescriptor} from unmapped XML names.
     *  The default is to have one that creates new {@link ElementDescriptor}. */
    private IUnknownDescriptorProvider mUnknownDescProvider;
    /** Error Flag */
    private boolean mHasError;

    /**
     * Creates a new {@link UiElementNode} described by a given {@link ElementDescriptor}.
     *
     * @param elementDescriptor The {@link ElementDescriptor} for the XML node. Cannot be null.
     */
    public UiElementNode(ElementDescriptor elementDescriptor) {
        mDescriptor = elementDescriptor;
        clearContent();
    }

    @Override
    public String toString() {
      return String.format("%s [desc: %s, parent: %s, children: %d]",         //$NON-NLS-1$
              this.getClass().getSimpleName(),
              mDescriptor,
              mUiParent != null ? mUiParent.toString() : "none",              //$NON-NLS-1$
                      mUiChildren != null ? mUiChildren.size() : 0
      );
    }

    /**
     * Clears the {@link UiElementNode} by resetting the children list and
     * the {@link UiAttributeNode}s list.
     * Also resets the attached XML node, document, editor if any.
     * <p/>
     * The parent {@link UiElementNode} node is not reset so that it's position
     * in the hierarchy be left intact, if any.
     */
    /* package */ void clearContent() {
        mXmlNode = null;
        mXmlDocument = null;
        mEditor = null;
        clearAttributes();
        mReadOnlyUiChildren = null;
        if (mUiChildren == null) {
            mUiChildren = new ArrayList<UiElementNode>();
        } else {
            // We can't remove mandatory nodes, we just clear them.
            for (int i = mUiChildren.size() - 1; i >= 0; --i) {
                removeUiChildAtIndex(i);
            }
        }
    }

    /**
     * Clears the internal list of attributes, the read-only cached version of it
     * and the read-only cached hidden attribute list.
     */
    private void clearAttributes() {
        mUiAttributes = null;
        mCachedAllUiAttributes = null;
        mCachedHiddenAttributes = null;
        mUnknownUiAttributes = new HashSet<UiAttributeNode>();
    }

    /**
     * Gets or creates the internal UiAttributes list.
     * <p/>
     * When the descriptor derives from ViewElementDescriptor, this list depends on the
     * current UiParent node.
     *
     * @return A new set of {@link UiAttributeNode} that matches the expected
     *         attributes for this node.
     */
    private HashMap<AttributeDescriptor, UiAttributeNode> getInternalUiAttributes() {
        if (mUiAttributes == null) {
            AttributeDescriptor[] attrList = getAttributeDescriptors();
            mUiAttributes = new HashMap<AttributeDescriptor, UiAttributeNode>(attrList.length);
            for (AttributeDescriptor desc : attrList) {
                UiAttributeNode uiNode = desc.createUiNode(this);
                if (uiNode != null) {  // Some AttributeDescriptors do not have UI associated
                    mUiAttributes.put(desc, uiNode);
                }
            }
        }
        return mUiAttributes;
    }

    /**
     * Computes a short string describing the UI node suitable for tree views.
     * Uses the element's attribute "android:name" if present, or the "android:label" one
     * followed by the element's name if not repeated.
     *
     * @return A short string describing the UI node suitable for tree views.
     */
    public String getShortDescription() {
        String name = mDescriptor.getUiName();
        String attr = getDescAttribute();
        if (attr != null) {
            // If the ui name is repeated in the attribute value, don't use it.
            // Typical case is to avoid ".pkg.MyActivity (Activity)".
            if (attr.contains(name)) {
                return attr;
            } else {
                return String.format("%1$s (%2$s)", attr, name);
            }
        }

        return name;
    }

    /** Returns the key attribute that can be used to describe this node, or null */
    private String getDescAttribute() {
        if (mXmlNode != null && mXmlNode instanceof Element && mXmlNode.hasAttributes()) {
            // Application and Manifest nodes have a special treatment: they are unique nodes
            // so we don't bother trying to differentiate their strings and we fall back to
            // just using the UI name below.
            Element elem = (Element) mXmlNode;

            String attr = _Element_getAttributeNS(elem,
                                SdkConstants.NS_RESOURCES,
                                AndroidManifestDescriptors.ANDROID_NAME_ATTR);
            if (attr == null || attr.length() == 0) {
                attr = _Element_getAttributeNS(elem,
                                SdkConstants.NS_RESOURCES,
                                AndroidManifestDescriptors.ANDROID_LABEL_ATTR);
            } else if (mXmlNode.getNodeName().equals(SdkConstants.VIEW_FRAGMENT)) {
                attr = attr.substring(attr.lastIndexOf('.') + 1);
            }
            if (attr == null || attr.length() == 0) {
                attr = _Element_getAttributeNS(elem,
                                SdkConstants.NS_RESOURCES,
                                OtherXmlDescriptors.PREF_KEY_ATTR);
            }
            if (attr == null || attr.length() == 0) {
                attr = _Element_getAttributeNS(elem,
                                null, // no namespace
                                SdkConstants.ATTR_NAME);
            }
            if (attr == null || attr.length() == 0) {
                attr = _Element_getAttributeNS(elem,
                                SdkConstants.NS_RESOURCES,
                                SdkConstants.ATTR_ID);

                if (attr != null && attr.length() > 0) {
                    for (String prefix : ID_PREFIXES) {
                        if (attr.startsWith(prefix)) {
                            attr = attr.substring(prefix.length());
                            break;
                        }
                    }
                }
            }
            if (attr != null && attr.length() > 0) {
                return attr;
            }
        }

        return null;
    }

    /**
     * Computes a styled string describing the UI node suitable for tree views.
     * Similar to {@link #getShortDescription()} but styles the Strings.
     *
     * @return A styled string describing the UI node suitable for tree views.
     */
    public StyledString getStyledDescription() {
        String uiName = mDescriptor.getUiName();

        // Special case: for <view>, show the class attribute value instead.
        // This is done here rather than in the descriptor since this depends on
        // node instance data.
        if (SdkConstants.VIEW_TAG.equals(uiName) && mXmlNode instanceof Element) {
            Element element = (Element) mXmlNode;
            String cls = element.getAttribute(ATTR_CLASS);
            if (cls != null) {
                uiName = cls.substring(cls.lastIndexOf('.') + 1);
            }
        }

        StyledString styledString = new StyledString();
        String attr = getDescAttribute();
        if (attr != null) {
            // Don't append the two when it's a repeat, e.g. Button01 (Button),
            // only when the ui name is not part of the attribute
            if (attr.toLowerCase(Locale.US).indexOf(uiName.toLowerCase(Locale.US)) == -1) {
                styledString.append(attr);
                styledString.append(String.format(" (%1$s)", uiName),
                        StyledString.DECORATIONS_STYLER);
            } else {
                styledString.append(attr);
            }
        }

        if (styledString.length() == 0) {
            styledString.append(uiName);
        }

        return styledString;
    }

    /**
     * Retrieves an attribute value by local name and namespace URI.
     * <br>Per [<a href='http://www.w3.org/TR/1999/REC-xml-names-19990114/'>XML Namespaces</a>]
     * , applications must use the value <code>null</code> as the
     * <code>namespaceURI</code> parameter for methods if they wish to have
     * no namespace.
     * <p/>
     * Note: This is a wrapper around {@link Element#getAttributeNS(String, String)}.
     * In some versions of webtools, the getAttributeNS implementation crashes with an NPE.
     * This wrapper will return an empty string instead.
     *
     * @see Element#getAttributeNS(String, String)
     * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108">https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108</a>
     * @return The result from {@link Element#getAttributeNS(String, String)} or an empty string.
     */
    private String _Element_getAttributeNS(Element element,
            String namespaceURI,
            String localName) {
        try {
            return element.getAttributeNS(namespaceURI, localName);
        } catch (Exception ignore) {
            return "";
        }
    }

    /**
     * Computes a "breadcrumb trail" description for this node.
     * It will look something like "Manifest > Application > .myactivity (Activity) > Intent-Filter"
     *
     * @param includeRoot Whether to include the root (e.g. "Manifest") or not. Has no effect
     *                     when called on the root node itself.
     * @return The "breadcrumb trail" description for this node.
     */
    public String getBreadcrumbTrailDescription(boolean includeRoot) {
        StringBuilder sb = new StringBuilder(getShortDescription());

        for (UiElementNode uiNode = getUiParent();
                uiNode != null;
                uiNode = uiNode.getUiParent()) {
            if (!includeRoot && uiNode.getUiParent() == null) {
                break;
            }
            sb.insert(0, String.format("%1$s > ", uiNode.getShortDescription())); //$NON-NLS-1$
        }

        return sb.toString();
    }

    /**
     * Sets the XML {@link Document}.
     * <p/>
     * The XML {@link Document} is initially null. The XML {@link Document} must be set only on the
     * UI root element node (this method takes care of that.)
     * @param xmlDoc The new XML document to associate this node with.
     */
    public void setXmlDocument(Document xmlDoc) {
        if (mUiParent == null) {
            mXmlDocument = xmlDoc;
        } else {
            mUiParent.setXmlDocument(xmlDoc);
        }
    }

    /**
     * Returns the XML {@link Document}.
     * <p/>
     * The value is initially null until the UI node is attached to its UI parent -- the value
     * of the document is then propagated.
     *
     * @return the XML {@link Document} or the parent's XML {@link Document} or null.
     */
    public Document getXmlDocument() {
        if (mXmlDocument != null) {
            return mXmlDocument;
        } else if (mUiParent != null) {
            return mUiParent.getXmlDocument();
        }
        return null;
    }

    /**
     * Returns the XML node associated with this UI node.
     * <p/>
     * Some {@link ElementDescriptor} are declared as being "mandatory". This means the
     * corresponding UI node will exist even if there is no corresponding XML node. Such structure
     * is created and enforced by the parent of the tree, not the element themselves. However
     * such nodes will likely not have an XML node associated, so getXmlNode() can return null.
     *
     * @return The associated XML node. Can be null for mandatory nodes.
     */
    public Node getXmlNode() {
        return mXmlNode;
    }

    /**
     * Returns the {@link ElementDescriptor} for this node. This is never null.
     * <p/>
     * Do not use this to call getDescriptor().getAttributes(), instead call
     * getAttributeDescriptors() which can be overridden by derived classes.
     * @return The {@link ElementDescriptor} for this node. This is never null.
     */
    public ElementDescriptor getDescriptor() {
        return mDescriptor;
    }

    /**
     * Returns the {@link AttributeDescriptor} array for the descriptor of this node.
     * <p/>
     * Use this instead of getDescriptor().getAttributes() -- derived classes can override
     * this to manipulate the attribute descriptor list depending on the current UI node.
     * @return The {@link AttributeDescriptor} array for the descriptor of this node.
     */
    public AttributeDescriptor[] getAttributeDescriptors() {
        return mDescriptor.getAttributes();
    }

    /**
     * Returns the hidden {@link AttributeDescriptor} array for the descriptor of this node.
     * This is a subset of the getAttributeDescriptors() list.
     * <p/>
     * Use this instead of getDescriptor().getHiddenAttributes() -- potentially derived classes
     * could override this to manipulate the attribute descriptor list depending on the current
     * UI node. There's no need for it right now so keep it private.
     */
    private Map<String, AttributeDescriptor> getHiddenAttributeDescriptors() {
        if (mCachedHiddenAttributes == null) {
            mCachedHiddenAttributes = new HashMap<String, AttributeDescriptor>();
            for (AttributeDescriptor attrDesc : getAttributeDescriptors()) {
                if (attrDesc instanceof XmlnsAttributeDescriptor) {
                    mCachedHiddenAttributes.put(
                            ((XmlnsAttributeDescriptor) attrDesc).getXmlNsName(),
                            attrDesc);
                }
            }
        }
        return mCachedHiddenAttributes;
    }

    /**
     * Sets the parent of this UiElementNode.
     * <p/>
     * The root node has no parent.
     */
    protected void setUiParent(UiElementNode parent) {
        mUiParent = parent;
        // Invalidate the internal UiAttributes list, as it may depend on the actual UiParent.
        clearAttributes();
    }

    /**
     * @return The parent {@link UiElementNode} or null if this is the root node.
     */
    public UiElementNode getUiParent() {
        return mUiParent;
    }

    /**
     * Returns the root {@link UiElementNode}.
     *
     * @return The root {@link UiElementNode}.
     */
    public UiElementNode getUiRoot() {
        UiElementNode root = this;
        while (root.mUiParent != null) {
            root = root.mUiParent;
        }

        return root;
    }

    /**
     * Returns the index of this sibling (where the first child has index 0, the second child
     * has index 1, and so on.)
     *
     * @return The sibling index of this node
     */
    public int getUiSiblingIndex() {
        if (mUiParent != null) {
            int index = 0;
            for (UiElementNode node : mUiParent.getUiChildren()) {
                if (node == this) {
                    break;
                }
                index++;
            }
            return index;
        }

        return 0;
    }

    /**
     * Returns the previous UI sibling of this UI node. If the node does not have a previous
     * sibling, returns null.
     *
     * @return The previous UI sibling of this UI node, or null if not applicable.
     */
    public UiElementNode getUiPreviousSibling() {
        if (mUiParent != null) {
            List<UiElementNode> childlist = mUiParent.getUiChildren();
            if (childlist != null && childlist.size() > 1 && childlist.get(0) != this) {
                int index = childlist.indexOf(this);
                return index > 0 ? childlist.get(index - 1) : null;
            }
        }
        return null;
    }

    /**
     * Returns the next UI sibling of this UI node.
     * If the node does not have a next sibling, returns null.
     *
     * @return The next UI sibling of this UI node, or null.
     */
    public UiElementNode getUiNextSibling() {
        if (mUiParent != null) {
            List<UiElementNode> childlist = mUiParent.getUiChildren();
            if (childlist != null) {
                int size = childlist.size();
                if (size > 1 && childlist.get(size - 1) != this) {
                    int index = childlist.indexOf(this);
                    return index >= 0 && index < size - 1 ? childlist.get(index + 1) : null;
                }
            }
        }
        return null;
    }

    /**
     * Sets the {@link AndroidXmlEditor} handling this {@link UiElementNode} hierarchy.
     * <p/>
     * The editor must always be set on the root node. This method takes care of that.
     *
     * @param editor The editor to associate this node with.
     */
    public void setEditor(AndroidXmlEditor editor) {
        if (mUiParent == null) {
            mEditor = editor;
        } else {
            mUiParent.setEditor(editor);
        }
    }

    /**
     * Returns the {@link AndroidXmlEditor} that embeds this {@link UiElementNode}.
     * <p/>
     * The value is initially null until the node is attached to its parent -- the value
     * of the root node is then propagated.
     *
     * @return The embedding {@link AndroidXmlEditor} or null.
     */
    public AndroidXmlEditor getEditor() {
        return mUiParent == null ? mEditor : mUiParent.getEditor();
    }

    /**
     * Returns the Android target data for the file being edited.
     *
     * @return The Android target data for the file being edited.
     */
    public AndroidTargetData getAndroidTarget() {
        return getEditor().getTargetData();
    }

    /**
     * @return A read-only version of the children collection.
     */
    public List<UiElementNode> getUiChildren() {
        if (mReadOnlyUiChildren == null) {
            mReadOnlyUiChildren = Collections.unmodifiableList(mUiChildren);
        }
        return mReadOnlyUiChildren;
    }

    /**
     * Returns a collection containing all the known attributes as well as
     * all the unknown ui attributes.
     *
     * @return A read-only version of the attributes collection.
     */
    public Collection<UiAttributeNode> getAllUiAttributes() {
        if (mCachedAllUiAttributes == null) {

            List<UiAttributeNode> allValues =
                new ArrayList<UiAttributeNode>(getInternalUiAttributes().values());
            allValues.addAll(mUnknownUiAttributes);

            mCachedAllUiAttributes = Collections.unmodifiableCollection(allValues);
        }
        return mCachedAllUiAttributes;
    }

    /**
     * Returns all the unknown ui attributes, that is those we found defined in the
     * actual XML but that we don't have descriptors for.
     *
     * @return A read-only version of the unknown attributes collection.
     */
    public Collection<UiAttributeNode> getUnknownUiAttributes() {
        return Collections.unmodifiableCollection(mUnknownUiAttributes);
    }

    /**
     * Sets the error flag value.
     *
     * @param errorFlag the error flag
     */
    public final void setHasError(boolean errorFlag) {
        mHasError = errorFlag;
    }

    /**
     * Returns whether this node, its attributes, or one of the children nodes (and attributes)
     * has errors.
     *
     * @return True if this node, its attributes, or one of the children nodes (and attributes)
     * has errors.
     */
    public final boolean hasError() {
        if (mHasError) {
            return true;
        }

        // get the error value from the attributes.
        for (UiAttributeNode attribute : getAllUiAttributes()) {
            if (attribute.hasError()) {
                return true;
            }
        }

        // and now from the children.
        for (UiElementNode child : mUiChildren) {
            if (child.hasError()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns the provider that knows how to create {@link ElementDescriptor} from unmapped
     * XML names.
     * <p/>
     * The default is to have one that creates new {@link ElementDescriptor}.
     * <p/>
     * There is only one such provider in any UI model tree, attached to the root node.
     *
     * @return An instance of {@link IUnknownDescriptorProvider}. Can never be null.
     */
    public IUnknownDescriptorProvider getUnknownDescriptorProvider() {
        if (mUiParent != null) {
            return mUiParent.getUnknownDescriptorProvider();
        }
        if (mUnknownDescProvider == null) {
            // Create the default one on demand.
            mUnknownDescProvider = new IUnknownDescriptorProvider() {

                private final HashMap<String, ElementDescriptor> mMap =
                    new HashMap<String, ElementDescriptor>();

                /**
                 * The default is to create a new ElementDescriptor wrapping
                 * the unknown XML local name and reuse previously created descriptors.
                 */
                @Override
                public ElementDescriptor getDescriptor(String xmlLocalName) {

                    ElementDescriptor desc = mMap.get(xmlLocalName);

                    if (desc == null) {
                        desc = new ElementDescriptor(xmlLocalName);
                        mMap.put(xmlLocalName, desc);
                    }

                    return desc;
                }
            };
        }
        return mUnknownDescProvider;
    }

    /**
     * Sets the provider that knows how to create {@link ElementDescriptor} from unmapped
     * XML names.
     * <p/>
     * The default is to have one that creates new {@link ElementDescriptor}.
     * <p/>
     * There is only one such provider in any UI model tree, attached to the root node.
     *
     * @param unknownDescProvider The new provider to use. Must not be null.
     */
    public void setUnknownDescriptorProvider(IUnknownDescriptorProvider unknownDescProvider) {
        if (mUiParent == null) {
            mUnknownDescProvider = unknownDescProvider;
        } else {
            mUiParent.setUnknownDescriptorProvider(unknownDescProvider);
        }
    }

    /**
     * Adds a new {@link IUiUpdateListener} to the internal update listener list.
     *
     * @param listener The listener to add.
     */
    public void addUpdateListener(IUiUpdateListener listener) {
       if (mUiUpdateListeners == null) {
           mUiUpdateListeners = new ArrayList<IUiUpdateListener>();
       }
       if (!mUiUpdateListeners.contains(listener)) {
           mUiUpdateListeners.add(listener);
       }
    }

    /**
     * Removes an existing {@link IUiUpdateListener} from the internal update listener list.
     * Does nothing if the list is empty or the listener is not registered.
     *
     * @param listener The listener to remove.
     */
    public void removeUpdateListener(IUiUpdateListener listener) {
       if (mUiUpdateListeners != null) {
           mUiUpdateListeners.remove(listener);
       }
    }

    /**
     * Finds a child node relative to this node using a path-like expression.
     * F.ex. "node1/node2" would find a child "node1" that contains a child "node2" and
     * returns the latter. If there are multiple nodes with the same name at the same
     * level, always uses the first one found.
     *
     * @param path The path like expression to select a child node.
     * @return The ui node found or null.
     */
    public UiElementNode findUiChildNode(String path) {
        String[] items = path.split("/");  //$NON-NLS-1$
        UiElementNode uiNode = this;
        for (String item : items) {
            boolean nextSegment = false;
            for (UiElementNode c : uiNode.mUiChildren) {
                if (c.getDescriptor().getXmlName().equals(item)) {
                    uiNode = c;
                    nextSegment = true;
                    break;
                }
            }
            if (!nextSegment) {
                return null;
            }
        }
        return uiNode;
    }

    /**
     * Finds an {@link UiElementNode} which contains the give XML {@link Node}.
     * Looks recursively in all children UI nodes.
     *
     * @param xmlNode The XML node to look for.
     * @return The {@link UiElementNode} that contains xmlNode or null if not found,
     */
    public UiElementNode findXmlNode(Node xmlNode) {
        if (xmlNode == null) {
            return null;
        }
        if (getXmlNode() == xmlNode) {
            return this;
        }

        for (UiElementNode uiChild : mUiChildren) {
            UiElementNode found = uiChild.findXmlNode(xmlNode);
            if (found != null) {
                return found;
            }
        }

        return null;
    }

    /**
     * Returns the {@link UiAttributeNode} matching this attribute descriptor or
     * null if not found.
     *
     * @param attrDesc The {@link AttributeDescriptor} to match.
     * @return the {@link UiAttributeNode} matching this attribute descriptor or null
     *         if not found.
     */
    public UiAttributeNode findUiAttribute(AttributeDescriptor attrDesc) {
        return getInternalUiAttributes().get(attrDesc);
    }

    /**
     * Populate this element node with all values from the given XML node.
     *
     * This fails if the given XML node has a different element name -- it won't change the
     * type of this ui node.
     *
     * This method can be both used for populating values the first time and updating values
     * after the XML model changed.
     *
     * @param xmlNode The XML node to mirror
     * @return Returns true if the XML structure has changed (nodes added, removed or replaced)
     */
    public boolean loadFromXmlNode(Node xmlNode) {
        boolean structureChanged = (mXmlNode != xmlNode);
        mXmlNode = xmlNode;
        if (xmlNode != null) {
            updateAttributeList(xmlNode);
            structureChanged |= updateElementList(xmlNode);
            invokeUiUpdateListeners(structureChanged ? UiUpdateState.CHILDREN_CHANGED
                                                      : UiUpdateState.ATTR_UPDATED);
        }
        return structureChanged;
    }

    /**
     * Clears the UI node and reload it from the given XML node.
     * <p/>
     * This works by clearing all references to any previous XML or UI nodes and
     * then reloads the XML document from scratch. The editor reference is kept.
     * <p/>
     * This is used in the special case where the ElementDescriptor structure has changed.
     * Rather than try to diff inflated UI nodes (as loadFromXmlNode does), we don't bother
     * and reload everything. This is not subtle and should be used very rarely.
     *
     * @param xmlNode The XML node or document to reload. Can be null.
     */
    public void reloadFromXmlNode(Node xmlNode) {
        // The editor needs to be preserved, it is not affected by an XML change.
        AndroidXmlEditor editor = getEditor();
        clearContent();
        setEditor(editor);
        if (xmlNode != null) {
            setXmlDocument(xmlNode.getOwnerDocument());
        }
        // This will reload all the XML and recreate the UI structure from scratch.
        loadFromXmlNode(xmlNode);
    }

    /**
     * Called by attributes when they want to commit their value
     * to an XML node.
     * <p/>
     * For mandatory nodes, this makes sure the underlying XML element node
     * exists in the model. If not, it is created and assigned as the underlying
     * XML node.
     * </br>
     * For non-mandatory nodes, simply return the underlying XML node, which
     * must always exists.
     *
     * @return The XML node matching this {@link UiElementNode} or null.
     */
    public Node prepareCommit() {
        if (getDescriptor().getMandatory() != Mandatory.NOT_MANDATORY) {
            createXmlNode();
            // The new XML node has been created.
            // We don't need to refresh using loadFromXmlNode() since there are
            // no attributes or elements that need to be loading into this node.
        }
        return getXmlNode();
    }

    /**
     * Commits the attributes (all internal, inherited from UI parent & unknown attributes).
     * This is called by the UI when the embedding part needs to be committed.
     */
    public void commit() {
        for (UiAttributeNode uiAttr : getAllUiAttributes()) {
            uiAttr.commit();
        }
    }

    /**
     * Returns true if the part has been modified with respect to the data
     * loaded from the model.
     * @return True if the part has been modified with respect to the data
     * loaded from the model.
     */
    public boolean isDirty() {
        for (UiAttributeNode uiAttr : getAllUiAttributes()) {
            if (uiAttr.isDirty()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Creates the underlying XML element node for this UI node if it doesn't already
     * exists.
     *
     * @return The new value of getXmlNode() (can be null if creation failed)
     */
    public Node createXmlNode() {
        if (mXmlNode != null) {
            return null;
        }
        Node parentXmlNode = null;
        if (mUiParent != null) {
            parentXmlNode = mUiParent.prepareCommit();
            if (parentXmlNode == null) {
                // The parent failed to create its own backing XML node. Abort.
                // No need to throw an exception, the parent will most likely
                // have done so itself.
                return null;
            }
        }

        String elementName = getDescriptor().getXmlName();
        Document doc = getXmlDocument();

        // We *must* have a root node. If not, we need to abort.
        if (doc == null) {
            throw new RuntimeException(
                    String.format("Missing XML document for %1$s XML node.", elementName));
        }

        // If we get here and parentXmlNode is null, the node is to be created
        // as the root node of the document (which can't be null, cf. check above).
        if (parentXmlNode == null) {
            parentXmlNode = doc;
        }

        mXmlNode = doc.createElement(elementName);

        // If this element does not have children, mark it as an empty tag
        // such that the XML looks like <tag/> instead of <tag></tag>
        if (!mDescriptor.hasChildren()) {
            if (mXmlNode instanceof ElementImpl) {
                ElementImpl element = (ElementImpl) mXmlNode;
                element.setEmptyTag(true);
            }
        }

        Node xmlNextSibling = null;

        UiElementNode uiNextSibling = getUiNextSibling();
        if (uiNextSibling != null) {
            xmlNextSibling = uiNextSibling.getXmlNode();
        }

        Node previousTextNode = null;
        if (xmlNextSibling != null) {
            Node previousNode = xmlNextSibling.getPreviousSibling();
            if (previousNode != null && previousNode.getNodeType() == Node.TEXT_NODE) {
                previousTextNode = previousNode;
            }
        } else {
            Node lastChild = parentXmlNode.getLastChild();
            if (lastChild != null && lastChild.getNodeType() == Node.TEXT_NODE) {
                previousTextNode = lastChild;
            }
        }

        String insertAfter = null;

        // Try to figure out the indentation node to insert. Even in auto-formatting
        // we need to do this, because it turns out the XML editor's formatter does
        // not do a very good job with completely botched up XML; it does a much better
        // job if the new XML is already mostly well formatted. Thus, the main purpose
        // of applying the real XML formatter after our own indentation attempts here is
        // to make it apply its own tab-versus-spaces indentation properties, have it
        // insert line breaks before attributes (if the user has configured that), etc.

        // First figure out the indentation level of the newly inserted element;
        // this is either the same as the previous sibling, or if there is no sibling,
        // it's the indentation of the parent plus one indentation level.
        boolean isFirstChild = getUiPreviousSibling() == null
                || parentXmlNode.getFirstChild() == null;
        AndroidXmlEditor editor = getEditor();
        String indent;
        String parentIndent = ""; //$NON-NLS-1$
        if (isFirstChild) {
            indent = parentIndent = editor.getIndent(parentXmlNode);
            // We need to add one level of indentation. Are we using tabs?
            // Can't get to formatting settings so let's just look at the
            // parent indentation and see if we can guess
            if (indent.length() > 0 && indent.charAt(indent.length()-1) == '\t') {
                indent = indent + '\t';
            } else {
                // Not using tabs, or we can't figure it out (because parent had no
                // indentation). In that case, indent with 4 spaces, as seems to
                // be the Android default.
                indent = indent + "    "; //$NON-NLS-1$
            }
        } else {
            // Find out the indent of the previous sibling
            indent = editor.getIndent(getUiPreviousSibling().getXmlNode());
        }

        // We want to insert the new element BEFORE the text node which precedes
        // the next element, since that text node is the next element's indentation!
        if (previousTextNode != null) {
            xmlNextSibling = previousTextNode;
        } else {
            // If there's no previous text node, we are probably inside an
            // empty element (<LinearLayout>|</LinearLayout>) and in that case we need
            // to not only insert a newline and indentation before the new element, but
            // after it as well.
            insertAfter = parentIndent;
        }

        // Insert indent text node before the new element
        IStructuredDocument document = editor.getStructuredDocument();
        String newLine;
        if (document != null) {
            newLine = TextUtilities.getDefaultLineDelimiter(document);
        } else {
            newLine = SdkUtils.getLineSeparator();
        }
        Text indentNode = doc.createTextNode(newLine + indent);
        parentXmlNode.insertBefore(indentNode, xmlNextSibling);

        // Insert the element itself
        parentXmlNode.insertBefore(mXmlNode, xmlNextSibling);

        // Insert a separator after the tag. We only do this when we've inserted
        // a tag into an area where there was no whitespace before
        // (e.g. a new child of <LinearLayout></LinearLayout>).
        if (insertAfter != null) {
            Text sep = doc.createTextNode(newLine + insertAfter);
            parentXmlNode.insertBefore(sep, xmlNextSibling);
        }

        // Set all initial attributes in the XML node if they are not empty.
        // Iterate on the descriptor list to get the desired order and then use the
        // internal values, if any.
        List<UiAttributeNode> addAttributes = new ArrayList<UiAttributeNode>();

        for (AttributeDescriptor attrDesc : getAttributeDescriptors()) {
            if (attrDesc instanceof XmlnsAttributeDescriptor) {
                XmlnsAttributeDescriptor desc = (XmlnsAttributeDescriptor) attrDesc;
                Attr attr = doc.createAttributeNS(SdkConstants.XMLNS_URI,
                        desc.getXmlNsName());
                attr.setValue(desc.getValue());
                attr.setPrefix(desc.getXmlNsPrefix());
                mXmlNode.getAttributes().setNamedItemNS(attr);
            } else {
                UiAttributeNode uiAttr = getInternalUiAttributes().get(attrDesc);

                // Don't apply the attribute immediately, instead record this attribute
                // such that we can gather all attributes and sort them first.
                // This is necessary because the XML model will *append* all attributes
                // so we want to add them in a particular order.
                // (Note that we only have to worry about UiAttributeNodes with non null
                // values, since this is a new node and we therefore don't need to attempt
                // to remove existing attributes)
                String value = uiAttr.getCurrentValue();
                if (value != null && value.length() > 0) {
                    addAttributes.add(uiAttr);
                }
            }
        }

        // Sort and apply the attributes in order, because the Eclipse XML model will always
        // append the XML attributes, so by inserting them in our desired order they will
        // appear that way in the XML
        Collections.sort(addAttributes);

        for (UiAttributeNode node : addAttributes) {
            commitAttributeToXml(node, node.getCurrentValue());
            node.setDirty(false);
        }

        getEditor().scheduleNodeReformat(this, false);

        // Notify per-node listeners
        invokeUiUpdateListeners(UiUpdateState.CREATED);
        // Notify global listeners
        fireNodeCreated(this, getUiSiblingIndex());

        return mXmlNode;
    }

    /**
     * Removes the XML node corresponding to this UI node if it exists
     * and also removes all mirrored information in this UI node (i.e. children, attributes)
     *
     * @return The removed node or null if it didn't exist in the first place.
     */
    public Node deleteXmlNode() {
        if (mXmlNode == null) {
            return null;
        }

        int previousIndex = getUiSiblingIndex();

        // First clear the internals of the node and *then* actually deletes the XML
        // node (because doing so will generate an update even and this node may be
        // revisited via loadFromXmlNode).
        Node oldXmlNode = mXmlNode;
        clearContent();

        Node xmlParent = oldXmlNode.getParentNode();
        if (xmlParent == null) {
            xmlParent = getXmlDocument();
        }
        Node previousSibling = oldXmlNode.getPreviousSibling();
        oldXmlNode = xmlParent.removeChild(oldXmlNode);

        // We need to remove the text node BEFORE the removed element, since THAT's the
        // indentation node for the removed element.
        if (previousSibling != null && previousSibling.getNodeType() == Node.TEXT_NODE
                && previousSibling.getNodeValue().trim().length() == 0) {
            xmlParent.removeChild(previousSibling);
        }

        invokeUiUpdateListeners(UiUpdateState.DELETED);
        fireNodeDeleted(this, previousIndex);

        return oldXmlNode;
    }

    /**
     * Updates the element list for this UiElementNode.
     * At the end, the list of children UiElementNode here will match the one from the
     * provided XML {@link Node}:
     * <ul>
     * <li> Walk both the current ui children list and the xml children list at the same time.
     * <li> If we have a new xml child but already reached the end of the ui child list, add the
     *      new xml node.
     * <li> Otherwise, check if the xml node is referenced later in the ui child list and if so,
     *      move it here. It means the XML child list has been reordered.
     * <li> Otherwise, this is a new XML node that we add in the middle of the ui child list.
     * <li> At the end, we may have finished walking the xml child list but still have remaining
     *      ui children, simply delete them as they matching trailing xml nodes that have been
     *      removed unless they are mandatory ui nodes.
     * </ul>
     * Note that only the first case is used when populating the ui list the first time.
     *
     * @param xmlNode The XML node to mirror
     * @return True when the XML structure has changed.
     */
    protected boolean updateElementList(Node xmlNode) {
        boolean structureChanged = false;
        boolean hasMandatoryLast = false;
        int uiIndex = 0;
        Node xmlChild = xmlNode.getFirstChild();
        while (xmlChild != null) {
            if (xmlChild.getNodeType() == Node.ELEMENT_NODE) {
                String elementName = xmlChild.getNodeName();
                UiElementNode uiNode = null;
                CustomViewDescriptorService service = CustomViewDescriptorService.getInstance();
                if (mUiChildren.size() <= uiIndex) {
                    // A new node is being added at the end of the list
                    ElementDescriptor desc = mDescriptor.findChildrenDescriptor(elementName,
                            false /* recursive */);
                    if (desc == null && elementName.indexOf('.') != -1 &&
                            (!elementName.startsWith(ANDROID_PKG_PREFIX)
                                    || elementName.startsWith(ANDROID_SUPPORT_PKG_PREFIX))) {
                        AndroidXmlEditor editor = getEditor();
                        if (editor != null && editor.getProject() != null) {
                            desc = service.getDescriptor(editor.getProject(), elementName);
                        }
                    }
                    if (desc == null) {
                        // Unknown node. Create a temporary descriptor for it.
                        // We'll add unknown attributes to it later.
                        IUnknownDescriptorProvider p = getUnknownDescriptorProvider();
                        desc = p.getDescriptor(elementName);
                    }
                    structureChanged = true;
                    uiNode = appendNewUiChild(desc);
                    uiIndex++;
                } else {
                    // A new node is being inserted or moved.
                    // Note: mandatory nodes can be created without an XML node in which case
                    // getXmlNode() is null.
                    UiElementNode uiChild;
                    int n = mUiChildren.size();
                    for (int j = uiIndex; j < n; j++) {
                        uiChild = mUiChildren.get(j);
                        if (uiChild.getXmlNode() != null && uiChild.getXmlNode() == xmlChild) {
                            if (j > uiIndex) {
                                // Found the same XML node at some later index, now move it here.
                                mUiChildren.remove(j);
                                mUiChildren.add(uiIndex, uiChild);
                                structureChanged = true;
                            }
                            uiNode = uiChild;
                            uiIndex++;
                            break;
                        }
                    }

                    if (uiNode == null) {
                        // Look for an unused mandatory node with no XML node attached
                        // referencing the same XML element name
                        for (int j = uiIndex; j < n; j++) {
                            uiChild = mUiChildren.get(j);
                            if (uiChild.getXmlNode() == null &&
                                    uiChild.getDescriptor().getMandatory() !=
                                                                Mandatory.NOT_MANDATORY &&
                                    uiChild.getDescriptor().getXmlName().equals(elementName)) {

                                if (j > uiIndex) {
                                    // Found it, now move it here
                                    mUiChildren.remove(j);
                                    mUiChildren.add(uiIndex, uiChild);
                                }
                                // Assign the XML node to this empty mandatory element.
                                uiChild.mXmlNode = xmlChild;
                                structureChanged = true;
                                uiNode = uiChild;
                                uiIndex++;
                            }
                        }
                    }

                    if (uiNode == null) {
                        // Inserting new node
                        ElementDescriptor desc = mDescriptor.findChildrenDescriptor(elementName,
                                false /* recursive */);
                        if (desc == null && elementName.indexOf('.') != -1 &&
                                (!elementName.startsWith(ANDROID_PKG_PREFIX)
                                        || elementName.startsWith(ANDROID_SUPPORT_PKG_PREFIX))) {
                            AndroidXmlEditor editor = getEditor();
                            if (editor != null && editor.getProject() != null) {
                                desc = service.getDescriptor(editor.getProject(), elementName);
                            }
                        }
                        if (desc == null) {
                            // Unknown node. Create a temporary descriptor for it.
                            // We'll add unknown attributes to it later.
                            IUnknownDescriptorProvider p = getUnknownDescriptorProvider();
                            desc = p.getDescriptor(elementName);
                        } else {
                            structureChanged = true;
                            uiNode = insertNewUiChild(uiIndex, desc);
                            uiIndex++;
                        }
                    }
                }
                if (uiNode != null) {
                    // If we touched an UI Node, even an existing one, refresh its content.
                    // For new nodes, this will populate them recursively.
                    structureChanged |= uiNode.loadFromXmlNode(xmlChild);

                    // Remember if there are any mandatory-last nodes to reorder.
                    hasMandatoryLast |=
                        uiNode.getDescriptor().getMandatory() == Mandatory.MANDATORY_LAST;
                }
            }
            xmlChild = xmlChild.getNextSibling();
        }

        // There might be extra UI nodes at the end if the XML node list got shorter.
        for (int index = mUiChildren.size() - 1; index >= uiIndex; --index) {
             structureChanged |= removeUiChildAtIndex(index);
        }

        if (hasMandatoryLast) {
            // At least one mandatory-last uiNode was moved. Let's see if we can
            // move them back to the last position. That's possible if the only
            // thing between these and the end are other mandatory empty uiNodes
            // (mandatory uiNodes with no XML attached are pure "virtual" reserved
            // slots and it's ok to reorganize them but other can't.)
            int n = mUiChildren.size() - 1;
            for (int index = n; index >= 0; index--) {
                UiElementNode uiChild = mUiChildren.get(index);
                Mandatory mand = uiChild.getDescriptor().getMandatory();
                if (mand == Mandatory.MANDATORY_LAST && index < n) {
                    // Remove it from index and move it back at the end of the list.
                    mUiChildren.remove(index);
                    mUiChildren.add(uiChild);
                } else if (mand == Mandatory.NOT_MANDATORY || uiChild.getXmlNode() != null) {
                    // We found at least one non-mandatory or a mandatory node with an actual
                    // XML attached, so there's nothing we can reorganize past this point.
                    break;
                }
            }
        }

        return structureChanged;
    }

    /**
     * Internal helper to remove an UI child node given by its index in the
     * internal child list.
     *
     * Also invokes the update listener on the node to be deleted *after* the node has
     * been removed.
     *
     * @param uiIndex The index of the UI child to remove, range 0 .. mUiChildren.size()-1
     * @return True if the structure has changed
     * @throws IndexOutOfBoundsException if index is out of mUiChildren's bounds. Of course you
     *         know that could never happen unless the computer is on fire or something.
     */
    private boolean removeUiChildAtIndex(int uiIndex) {
        UiElementNode uiNode = mUiChildren.get(uiIndex);
        ElementDescriptor desc = uiNode.getDescriptor();

        try {
            if (uiNode.getDescriptor().getMandatory() != Mandatory.NOT_MANDATORY) {
                // This is a mandatory node. Such a node must exist in the UiNode hierarchy
                // even if there's no XML counterpart. However we only need to keep one.

                // Check if the parent (e.g. this node) has another similar ui child node.
                boolean keepNode = true;
                for (UiElementNode child : mUiChildren) {
                    if (child != uiNode && child.getDescriptor() == desc) {
                        // We found another child with the same descriptor that is not
                        // the node we want to remove. This means we have one mandatory
                        // node so we can safely remove uiNode.
                        keepNode = false;
                        break;
                    }
                }

                if (keepNode) {
                    // We can't remove a mandatory node as we need to keep at least one
                    // mandatory node in the parent. Instead we just clear its content
                    // (including its XML Node reference).

                    // A mandatory node with no XML means it doesn't really exist, so it can't be
                    // deleted. So the structure will change only if the ui node is actually
                    // associated to an XML node.
                    boolean xmlExists = (uiNode.getXmlNode() != null);

                    uiNode.clearContent();
                    return xmlExists;
                }
            }

            mUiChildren.remove(uiIndex);

            return true;
        } finally {
            // Tell listeners that a node has been removed.
            // The model has already been modified.
            invokeUiUpdateListeners(UiUpdateState.DELETED);
        }
    }

    /**
     * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor}
     * and appends it to the end of the element children list.
     *
     * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node.
     * @return The new UI node that has been appended
     */
    public UiElementNode appendNewUiChild(ElementDescriptor descriptor) {
        UiElementNode uiNode;
        uiNode = descriptor.createUiNode();
        mUiChildren.add(uiNode);
        uiNode.setUiParent(this);
        uiNode.invokeUiUpdateListeners(UiUpdateState.CREATED);
        return uiNode;
    }

    /**
     * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor}
     * and inserts it in the element children list at the specified position.
     *
     * @param index The position where to insert in the element children list.
     *              Shifts the element currently at that position (if any) and any
     *              subsequent elements to the right (adds one to their indices).
     *              Index must >= 0 and <= getUiChildren.size().
     *              Using size() means to append to the end of the list.
     * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node.
     * @return The new UI node.
     */
    public UiElementNode insertNewUiChild(int index, ElementDescriptor descriptor) {
        UiElementNode uiNode;
        uiNode = descriptor.createUiNode();
        mUiChildren.add(index, uiNode);
        uiNode.setUiParent(this);
        uiNode.invokeUiUpdateListeners(UiUpdateState.CREATED);
        return uiNode;
    }

    /**
     * Updates the {@link UiAttributeNode} list for this {@link UiElementNode}
     * using the values from the XML element.
     * <p/>
     * For a given {@link UiElementNode}, the attribute list always exists in
     * full and is totally independent of whether the XML model actually
     * has the corresponding attributes.
     * <p/>
     * For each attribute declared in this {@link UiElementNode}, get
     * the corresponding XML attribute. It may not exist, in which case the
     * value will be null. We don't really know if a value has changed, so
     * the updateValue() is called on the UI attribute in all cases.
     *
     * @param xmlNode The XML node to mirror
     */
    protected void updateAttributeList(Node xmlNode) {
        NamedNodeMap xmlAttrMap = xmlNode.getAttributes();
        HashSet<Node> visited = new HashSet<Node>();

        // For all known (i.e. expected) UI attributes, find an existing XML attribute of
        // same (uri, local name) and update the internal Ui attribute value.
        for (UiAttributeNode uiAttr : getInternalUiAttributes().values()) {
            AttributeDescriptor desc = uiAttr.getDescriptor();
            if (!(desc instanceof SeparatorAttributeDescriptor)) {
                Node xmlAttr = xmlAttrMap == null ? null :
                    xmlAttrMap.getNamedItemNS(desc.getNamespaceUri(), desc.getXmlLocalName());
                uiAttr.updateValue(xmlAttr);
                visited.add(xmlAttr);
            }
        }

        // Clone the current list of unknown attributes. We'll then remove from this list when
        // we find attributes which are still unknown. What will be left are the old unknown
        // attributes that have been deleted in the current XML attribute list.
        @SuppressWarnings("unchecked")
        HashSet<UiAttributeNode> deleted = (HashSet<UiAttributeNode>) mUnknownUiAttributes.clone();

        // We need to ignore hidden attributes.
        Map<String, AttributeDescriptor> hiddenAttrDesc = getHiddenAttributeDescriptors();

        // Traverse the actual XML attribute list to find unknown attributes
        if (xmlAttrMap != null) {
            for (int i = 0; i < xmlAttrMap.getLength(); i++) {
                Node xmlAttr = xmlAttrMap.item(i);
                // Ignore attributes which have actual descriptors
                if (visited.contains(xmlAttr)) {
                    continue;
                }

                String xmlFullName = xmlAttr.getNodeName();

                // Ignore attributes which are hidden (based on the prefix:localName key)
                if (hiddenAttrDesc.containsKey(xmlFullName)) {
                    continue;
                }

                String xmlAttrLocalName = xmlAttr.getLocalName();
                String xmlNsUri = xmlAttr.getNamespaceURI();

                UiAttributeNode uiAttr = null;
                for (UiAttributeNode a : mUnknownUiAttributes) {
                    String aLocalName = a.getDescriptor().getXmlLocalName();
                    String aNsUri = a.getDescriptor().getNamespaceUri();
                    if (aLocalName.equals(xmlAttrLocalName) &&
                            (aNsUri == xmlNsUri || (aNsUri != null && aNsUri.equals(xmlNsUri)))) {
                        // This attribute is still present in the unknown list
                        uiAttr = a;
                        // It has not been deleted
                        deleted.remove(a);
                        break;
                    }
                }
                if (uiAttr == null) {
                    uiAttr = addUnknownAttribute(xmlFullName, xmlAttrLocalName, xmlNsUri);
                }

                uiAttr.updateValue(xmlAttr);
            }

            // Remove from the internal list unknown attributes that have been deleted from the xml
            for (UiAttributeNode a : deleted) {
                mUnknownUiAttributes.remove(a);
                mCachedAllUiAttributes = null;
            }
        }
    }

    /**
     * Create a new temporary text attribute descriptor for the unknown attribute
     * and returns a new {@link UiAttributeNode} associated to this descriptor.
     * <p/>
     * The attribute is not marked as dirty, doing so is up to the caller.
     */
    private UiAttributeNode addUnknownAttribute(String xmlFullName,
            String xmlAttrLocalName, String xmlNsUri) {
        // Create a new unknown attribute of format string
        TextAttributeDescriptor desc = new TextAttributeDescriptor(
                xmlAttrLocalName,           // xml name
                xmlNsUri,                // ui name
                new AttributeInfo(xmlAttrLocalName, Format.STRING_SET)
                );
        UiAttributeNode uiAttr = desc.createUiNode(this);
        mUnknownUiAttributes.add(uiAttr);
        mCachedAllUiAttributes = null;
        return uiAttr;
    }

    /**
     * Invoke all registered {@link IUiUpdateListener} listening on this UI update for this node.
     */
    protected void invokeUiUpdateListeners(UiUpdateState state) {
        if (mUiUpdateListeners != null) {
            for (IUiUpdateListener listener : mUiUpdateListeners) {
                try {
                    listener.uiElementNodeUpdated(this, state);
                } catch (Exception e) {
                    // prevent a crashing listener from crashing the whole invocation chain
                    AdtPlugin.log(e, "UIElement Listener failed: %s, state=%s",  //$NON-NLS-1$
                            getBreadcrumbTrailDescription(true),
                            state.toString());
                }
            }
        }
    }

    // --- for derived implementations only ---

    @VisibleForTesting
    public void setXmlNode(Node xmlNode) {
        mXmlNode = xmlNode;
    }

    public void refreshUi() {
        invokeUiUpdateListeners(UiUpdateState.ATTR_UPDATED);
    }


    // ------------- Helpers

    /**
     * Helper method to commit a single attribute value to XML.
     * <p/>
     * This method updates the XML regardless of the current XML value.
     * Callers should check first if an update is needed.
     * If the new value is empty, the XML attribute will be actually removed.
     * <p/>
     * Note that the caller MUST ensure that modifying the underlying XML model is
     * safe and must take care of marking the model as dirty if necessary.
     *
     * @see AndroidXmlEditor#wrapEditXmlModel(Runnable)
     *
     * @param uiAttr The attribute node to commit. Must be a child of this UiElementNode.
     * @param newValue The new value to set.
     * @return True if the XML attribute was modified or removed, false if nothing changed.
     */
    public boolean commitAttributeToXml(UiAttributeNode uiAttr, String newValue) {
        // Get (or create) the underlying XML element node that contains the attributes.
        Node element = prepareCommit();
        if (element != null && uiAttr != null) {
            String attrLocalName = uiAttr.getDescriptor().getXmlLocalName();
            String attrNsUri = uiAttr.getDescriptor().getNamespaceUri();

            NamedNodeMap attrMap = element.getAttributes();
            if (newValue == null || newValue.length() == 0) {
                // Remove attribute if it's empty
                if (attrMap.getNamedItemNS(attrNsUri, attrLocalName) != null) {
                    attrMap.removeNamedItemNS(attrNsUri, attrLocalName);
                    return true;
                }
            } else {
                // Add or replace an attribute
                Document doc = element.getOwnerDocument();
                if (doc != null) {
                    Attr attr;
                    if (attrNsUri != null && attrNsUri.length() > 0) {
                        attr = (Attr) attrMap.getNamedItemNS(attrNsUri, attrLocalName);
                        if (attr == null) {
                            attr = doc.createAttributeNS(attrNsUri, attrLocalName);
                            attr.setPrefix(XmlUtils.lookupNamespacePrefix(element, attrNsUri));
                            attrMap.setNamedItemNS(attr);
                        }
                    } else {
                        attr = (Attr) attrMap.getNamedItem(attrLocalName);
                        if (attr == null) {
                            attr = doc.createAttribute(attrLocalName);
                            attrMap.setNamedItem(attr);
                        }
                    }
                    attr.setValue(newValue);
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Helper method to commit all dirty attributes values to XML.
     * <p/>
     * This method is useful if {@link #setAttributeValue(String, String, String, boolean)} has
     * been called more than once and all the attributes marked as dirty must be committed to
     * the XML. It calls {@link #commitAttributeToXml(UiAttributeNode, String)} on each dirty
     * attribute.
     * <p/>
     * Note that the caller MUST ensure that modifying the underlying XML model is
     * safe and must take care of marking the model as dirty if necessary.
     *
     * @see AndroidXmlEditor#wrapEditXmlModel(Runnable)
     *
     * @return True if one or more values were actually modified or removed,
     *         false if nothing changed.
     */
    @SuppressWarnings("null") // Eclipse is confused by the logic and gets it wrong
    public boolean commitDirtyAttributesToXml() {
        boolean result = false;
        List<UiAttributeNode> dirtyAttributes = new ArrayList<UiAttributeNode>();
        for (UiAttributeNode uiAttr : getAllUiAttributes()) {
            if (uiAttr.isDirty()) {
                String value = uiAttr.getCurrentValue();
                if (value != null && value.length() > 0) {
                    // Defer the new attributes: set these last and in order
                    dirtyAttributes.add(uiAttr);
                } else {
                    result |= commitAttributeToXml(uiAttr, value);
                    uiAttr.setDirty(false);
                }
            }
        }
        if (dirtyAttributes.size() > 0) {
            result = true;

            Collections.sort(dirtyAttributes);

            // The Eclipse XML model will *always* append new attributes.
            // Therefore, if any of the dirty attributes are new, they will appear
            // after any existing, clean attributes on the element. To fix this,
            // we need to first remove any of these attributes, then insert them
            // back in the right order.
            Node element = prepareCommit();
            if (element == null) {
                return result;
            }

            if (AdtPrefs.getPrefs().getFormatGuiXml() && getEditor().supportsFormatOnGuiEdit()) {
                // If auto formatting, don't bother with attribute sorting here since the
                // order will be corrected as soon as the edit is committed anyway
                for (UiAttributeNode uiAttribute : dirtyAttributes) {
                    commitAttributeToXml(uiAttribute, uiAttribute.getCurrentValue());
                    uiAttribute.setDirty(false);
                }

                return result;
            }

            AttributeDescriptor descriptor = dirtyAttributes.get(0).getDescriptor();
            String firstName = descriptor.getXmlLocalName();
            String firstNamePrefix = null;
            String namespaceUri = descriptor.getNamespaceUri();
            if (namespaceUri != null) {
                firstNamePrefix = XmlUtils.lookupNamespacePrefix(element, namespaceUri);
            }
            NamedNodeMap attributes = ((Element) element).getAttributes();
            List<Attr> move = new ArrayList<Attr>();
            for (int i = 0, n = attributes.getLength(); i < n; i++) {
                Attr attribute = (Attr) attributes.item(i);
                if (XmlAttributeSortOrder.compareAttributes(
                        attribute.getPrefix(), attribute.getLocalName(),
                        firstNamePrefix, firstName) > 0) {
                    move.add(attribute);
                }
            }

            for (Attr attribute : move) {
                if (attribute.getNamespaceURI() != null) {
                    attributes.removeNamedItemNS(attribute.getNamespaceURI(),
                            attribute.getLocalName());
                } else {
                    attributes.removeNamedItem(attribute.getName());
                }
            }

            // Merge back the removed DOM attribute nodes and the new UI attribute nodes.
            // In cases where the attribute DOM name and the UI attribute names equal,
            // skip the DOM nodes and just apply the UI attributes.
            int domAttributeIndex = 0;
            int domAttributeIndexMax = move.size();
            int uiAttributeIndex = 0;
            int uiAttributeIndexMax = dirtyAttributes.size();

            while (true) {
                Attr domAttribute;
                UiAttributeNode uiAttribute;

                int compare;
                if (uiAttributeIndex < uiAttributeIndexMax) {
                    if (domAttributeIndex < domAttributeIndexMax) {
                        domAttribute = move.get(domAttributeIndex);
                        uiAttribute = dirtyAttributes.get(uiAttributeIndex);

                        String domAttributeName = domAttribute.getLocalName();
                        String uiAttributeName = uiAttribute.getDescriptor().getXmlLocalName();
                        compare = XmlAttributeSortOrder.compareAttributes(domAttributeName,
                                uiAttributeName);
                    } else {
                        compare = 1;
                        uiAttribute = dirtyAttributes.get(uiAttributeIndex);
                        domAttribute = null;
                    }
                } else if (domAttributeIndex < domAttributeIndexMax) {
                    compare = -1;
                    domAttribute = move.get(domAttributeIndex);
                    uiAttribute = null;
                } else {
                    break;
                }

                if (compare < 0) {
                    if (domAttribute.getNamespaceURI() != null) {
                        attributes.setNamedItemNS(domAttribute);
                    } else {
                        attributes.setNamedItem(domAttribute);
                    }
                    domAttributeIndex++;
                } else {
                    assert compare >= 0;
                    if (compare == 0) {
                        domAttributeIndex++;
                    }
                    commitAttributeToXml(uiAttribute, uiAttribute.getCurrentValue());
                    uiAttribute.setDirty(false);
                    uiAttributeIndex++;
                }
            }
        }

        return result;
    }

    /**
     * Utility method to internally set the value of a text attribute for the current
     * UiElementNode.
     * <p/>
     * This method is a helper. It silently ignores the errors such as the requested
     * attribute not being present in the element or attribute not being settable.
     * It accepts inherited attributes (such as layout).
     * <p/>
     * This does not commit to the XML model. It does mark the attribute node as dirty.
     * This is up to the caller.
     *
     * @see #commitAttributeToXml(UiAttributeNode, String)
     * @see #commitDirtyAttributesToXml()
     *
     * @param attrXmlName The XML <em>local</em> name of the attribute to modify
     * @param attrNsUri The namespace URI of the attribute.
     *                  Can be null if the attribute uses the global namespace.
     * @param value The new value for the attribute. If set to null, the attribute is removed.
     * @param override True if the value must be set even if one already exists.
     * @return The {@link UiAttributeNode} that has been modified or null.
     */
    public UiAttributeNode setAttributeValue(
            String attrXmlName,
            String attrNsUri,
            String value,
            boolean override) {
        if (value == null) {
            value = ""; //$NON-NLS-1$ -- this removes an attribute
        }

        getEditor().scheduleNodeReformat(this, true);

        // Try with all internal attributes
        UiAttributeNode uiAttr = setInternalAttrValue(
                getAllUiAttributes(), attrXmlName, attrNsUri, value, override);
        if (uiAttr != null) {
            return uiAttr;
        }

        if (uiAttr == null) {
            // Failed to find the attribute. For non-android attributes that is mostly expected,
            // in which case we just create a new custom one. As a side effect, we'll find the
            // attribute descriptor via getAllUiAttributes().
            addUnknownAttribute(attrXmlName, attrXmlName, attrNsUri);

            // We've created the attribute, but not actually set the value on it, so let's do it.
            // Try with the updated internal attributes.
            // Implementation detail: we could just do a setCurrentValue + setDirty on the
            // uiAttr returned by addUnknownAttribute(); however going through setInternalAttrValue
            // means we won't duplicate the logic, at the expense of doing one more lookup.
            uiAttr = setInternalAttrValue(
                    getAllUiAttributes(), attrXmlName, attrNsUri, value, override);
        }

        return uiAttr;
    }

    private UiAttributeNode setInternalAttrValue(
            Collection<UiAttributeNode> attributes,
            String attrXmlName,
            String attrNsUri,
            String value,
            boolean override) {

        // For namespace less attributes (like the "layout" attribute of an <include> tag
        // we may be passed "" as the namespace (during an attribute copy), and it
        // should really be null instead.
        if (attrNsUri != null && attrNsUri.length() == 0) {
            attrNsUri = null;
        }

        for (UiAttributeNode uiAttr : attributes) {
            AttributeDescriptor uiDesc = uiAttr.getDescriptor();

            if (uiDesc.getXmlLocalName().equals(attrXmlName)) {
                // Both NS URI must be either null or equal.
                if ((attrNsUri == null && uiDesc.getNamespaceUri() == null) ||
                        (attrNsUri != null && attrNsUri.equals(uiDesc.getNamespaceUri()))) {

                    // Not all attributes are editable, ignore those which are not.
                    if (uiAttr instanceof IUiSettableAttributeNode) {
                        String current = uiAttr.getCurrentValue();
                        // Only update (and mark as dirty) if the attribute did not have any
                        // value or if the value was different.
                        if (override || current == null || !current.equals(value)) {
                            ((IUiSettableAttributeNode) uiAttr).setCurrentValue(value);
                            // mark the attribute as dirty since their internal content
                            // as been modified, but not the underlying XML model
                            uiAttr.setDirty(true);
                            return uiAttr;
                        }
                    }

                    // We found the attribute but it's not settable. Since attributes are
                    // not duplicated, just abandon here.
                    break;
                }
            }
        }

        return null;
    }

    /**
     * Utility method to retrieve the internal value of an attribute.
     * <p/>
     * Note that this retrieves the *field* value if the attribute has some UI, and
     * not the actual XML value. They may differ if the attribute is dirty.
     *
     * @param attrXmlName The XML name of the attribute to modify
     * @return The current internal value for the attribute or null in case of error.
     */
    public String getAttributeValue(String attrXmlName) {
        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();

        for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) {
            AttributeDescriptor uiDesc = entry.getKey();
            if (uiDesc.getXmlLocalName().equals(attrXmlName)) {
                UiAttributeNode uiAttr = entry.getValue();
                return uiAttr.getCurrentValue();
            }
        }
        return null;
    }

    // ------ IPropertySource methods

    @Override
    public Object getEditableValue() {
        return null;
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyDescriptors()
     *
     * Returns the property descriptor for this node. Since the descriptors are not linked to the
     * data, the AttributeDescriptor are used directly.
     */
    @Override
    public IPropertyDescriptor[] getPropertyDescriptors() {
        List<IPropertyDescriptor> propDescs = new ArrayList<IPropertyDescriptor>();

        // get the standard descriptors
        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
        Set<AttributeDescriptor> keys = attributeMap.keySet();


        // we only want the descriptor that do implement the IPropertyDescriptor interface.
        for (AttributeDescriptor key : keys) {
            if (key instanceof IPropertyDescriptor) {
                propDescs.add((IPropertyDescriptor)key);
            }
        }

        // now get the descriptor from the unknown attributes
        for (UiAttributeNode unknownNode : mUnknownUiAttributes) {
            if (unknownNode.getDescriptor() instanceof IPropertyDescriptor) {
                propDescs.add((IPropertyDescriptor)unknownNode.getDescriptor());
            }
        }

        // TODO cache this maybe, as it's not going to change (except for unknown descriptors)
        return propDescs.toArray(new IPropertyDescriptor[propDescs.size()]);
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyValue(java.lang.Object)
     *
     * Returns the value of a given property. The id is the result of IPropertyDescriptor.getId(),
     * which return the AttributeDescriptor itself.
     */
    @Override
    public Object getPropertyValue(Object id) {
        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();

        UiAttributeNode attribute = attributeMap.get(id);

        if (attribute == null) {
            // look for the id in the unknown attributes.
            for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
                if (id == unknownAttr.getDescriptor()) {
                    return unknownAttr;
                }
            }
        }

        return attribute;
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.ui.views.properties.IPropertySource#isPropertySet(java.lang.Object)
     *
     * Returns whether the property is set. In our case this is if the string is non empty.
     */
    @Override
    public boolean isPropertySet(Object id) {
        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();

        UiAttributeNode attribute = attributeMap.get(id);

        if (attribute != null) {
            return attribute.getCurrentValue().length() > 0;
        }

        // look for the id in the unknown attributes.
        for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
            if (id == unknownAttr.getDescriptor()) {
                return unknownAttr.getCurrentValue().length() > 0;
            }
        }

        return false;
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.ui.views.properties.IPropertySource#resetPropertyValue(java.lang.Object)
     *
     * Reset the property to its default value. For now we simply empty it.
     */
    @Override
    public void resetPropertyValue(Object id) {
        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();

        UiAttributeNode attribute = attributeMap.get(id);
        if (attribute != null) {
            // TODO: reset the value of the attribute

            return;
        }

        // look for the id in the unknown attributes.
        for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
            if (id == unknownAttr.getDescriptor()) {
                // TODO: reset the value of the attribute

                return;
            }
        }
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.ui.views.properties.IPropertySource#setPropertyValue(java.lang.Object, java.lang.Object)
     *
     * Set the property value. id is the result of IPropertyDescriptor.getId(), which is the
     * AttributeDescriptor itself. Value should be a String.
     */
    @Override
    public void setPropertyValue(Object id, Object value) {
        HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();

        UiAttributeNode attribute = attributeMap.get(id);

        if (attribute == null) {
            // look for the id in the unknown attributes.
            for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
                if (id == unknownAttr.getDescriptor()) {
                    attribute = unknownAttr;
                    break;
                }
            }
        }

        if (attribute != null) {

            // get the current value and compare it to the new value
            String oldValue = attribute.getCurrentValue();
            final String newValue = (String)value;

            if (oldValue.equals(newValue)) {
                return;
            }

            final UiAttributeNode fAttribute = attribute;
            AndroidXmlEditor editor = getEditor();
            editor.wrapEditXmlModel(new Runnable() {
                @Override
                public void run() {
                    commitAttributeToXml(fAttribute, newValue);
                }
            });
        }
    }

    /**
     * Returns true if this node is an ancestor (parent, grandparent, and so on)
     * of the given node. Note that a node is not considered an ancestor of
     * itself.
     *
     * @param node the node to test
     * @return true if this node is an ancestor of the given node
     */
    public boolean isAncestorOf(UiElementNode node) {
        node = node.getUiParent();
        while (node != null) {
            if (node == this) {
                return true;
            }
            node = node.getUiParent();
        }
        return false;
    }

    /**
     * Finds the nearest common parent of the two given nodes (which could be one of the
     * two nodes as well)
     *
     * @param node1 the first node to test
     * @param node2 the second node to test
     * @return the nearest common parent of the two given nodes
     */
    public static UiElementNode getCommonAncestor(UiElementNode node1, UiElementNode node2) {
        while (node2 != null) {
            UiElementNode current = node1;
            while (current != null && current != node2) {
                current = current.getUiParent();
            }
            if (current == node2) {
                return current;
            }
            node2 = node2.getUiParent();
        }

        return null;
    }

    // ---- Global node create/delete Listeners ----

    /** List of listeners to be notified of newly created nodes, or null */
    private static List<NodeCreationListener> sListeners;

    /** Notify listeners that a new node has been created */
    private void fireNodeCreated(UiElementNode newChild, int index) {
        // Nothing to do if there aren't any listeners. We don't need to worry about
        // the case where one thread is firing node changes while another is adding a listener
        // (in that case it's still okay for this node firing not to be heard) so perform
        // the check outside of synchronization.
        if (sListeners == null) {
            return;
        }
        synchronized (UiElementNode.class) {
            if (sListeners != null) {
                UiElementNode parent = newChild.getUiParent();
                for (NodeCreationListener listener : sListeners) {
                    listener.nodeCreated(parent, newChild, index);
                }
            }
        }
    }

    /** Notify listeners that a new node has been deleted */
    private void fireNodeDeleted(UiElementNode oldChild, int index) {
        if (sListeners == null) {
            return;
        }
        synchronized (UiElementNode.class) {
            if (sListeners != null) {
                UiElementNode parent = oldChild.getUiParent();
                for (NodeCreationListener listener : sListeners) {
                    listener.nodeDeleted(parent, oldChild, index);
                }
            }
        }
    }

    /**
     * Adds a {@link NodeCreationListener} to be notified when new nodes are created
     *
     * @param listener the listener to be notified
     */
    public static void addNodeCreationListener(NodeCreationListener listener) {
        synchronized (UiElementNode.class) {
            if (sListeners == null) {
                sListeners = new ArrayList<NodeCreationListener>(1);
            }
            sListeners.add(listener);
        }
    }

    /**
     * Removes a {@link NodeCreationListener} from the set of listeners such that it is
     * no longer notified when nodes are created.
     *
     * @param listener the listener to be removed from the notification list
     */
    public static void removeNodeCreationListener(NodeCreationListener listener) {
        synchronized (UiElementNode.class) {
            sListeners.remove(listener);
            if (sListeners.size() == 0) {
                sListeners = null;
            }
        }
    }

    /** Interface implemented by listeners to be notified of newly created nodes */
    public interface NodeCreationListener {
        /**
         * Called when a new child node is created and added to the given parent
         *
         * @param parent the parent of the created node
         * @param child the newly node
         * @param index the index among the siblings of the child <b>after</b>
         *            insertion
         */
        void nodeCreated(UiElementNode parent, UiElementNode child, int index);

        /**
         * Called when a child node is removed from the given parent
         *
         * @param parent the parent of the removed node
         * @param child the removed node
         * @param previousIndex the index among the siblings of the child
         *            <b>before</b> removal
         */
        void nodeDeleted(UiElementNode parent, UiElementNode child, int previousIndex);
    }
}