aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/relative/DeletionHandler.java
blob: 3eac510dfbaed6c2fe8354613c6a397e8d69f7dc (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
/*
 * Copyright (C) 2012 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.common.layout.relative;

import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.ID_PREFIX;
import static com.android.SdkConstants.NEW_ID_PREFIX;
import static com.android.ide.common.layout.BaseViewRule.stripIdPrefix;
import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_CENTER_HORIZONTAL;
import static com.android.ide.common.layout.relative.ConstraintType.LAYOUT_CENTER_VERTICAL;

import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.api.INode;
import com.android.ide.common.api.INode.IAttribute;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Handles deletions in a relative layout, transferring constraints across
 * deleted nodes
 * <p>
 * TODO: Consider adding the
 * {@link SdkConstants#ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING} attribute to a
 * node if it's pointing to a node which is deleted and which has no transitive
 * reference to another node.
 */
public class DeletionHandler {
    private final INode mLayout;
    private final INode[] mChildren;
    private final List<INode> mDeleted;
    private final Set<String> mDeletedIds;
    private final Map<String, INode> mNodeMap;
    private final List<INode> mMoved;

    /**
     * Creates a new {@link DeletionHandler}
     *
     * @param deleted the deleted nodes
     * @param moved nodes that were moved (e.g. deleted, but also inserted elsewhere)
     * @param layout the parent layout of the deleted nodes
     */
    public DeletionHandler(@NonNull List<INode> deleted, @NonNull List<INode> moved,
            @NonNull INode layout) {
        mDeleted = deleted;
        mMoved = moved;
        mLayout = layout;

        mChildren = mLayout.getChildren();
        mNodeMap = Maps.newHashMapWithExpectedSize(mChildren.length);
        for (INode child : mChildren) {
            String id = child.getStringAttr(ANDROID_URI, ATTR_ID);
            if (id != null) {
                mNodeMap.put(stripIdPrefix(id), child);
            }
        }

        mDeletedIds = Sets.newHashSetWithExpectedSize(mDeleted.size());
        for (INode node : mDeleted) {
            String id = node.getStringAttr(ANDROID_URI, ATTR_ID);
            if (id != null) {
                mDeletedIds.add(stripIdPrefix(id));
            }
        }

        // Any widgets that remain (e.g. typically because they were moved) should
        // keep their incoming dependencies
        for (INode node : mMoved) {
            String id = node.getStringAttr(ANDROID_URI, ATTR_ID);
            if (id != null) {
                mDeletedIds.remove(stripIdPrefix(id));
            }
        }
    }

    @Nullable
    private static String getId(@NonNull IAttribute attribute) {
        if (attribute.getName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
                && ANDROID_URI.equals(attribute.getUri())
                && !attribute.getName().startsWith(ATTR_LAYOUT_MARGIN)) {
            String id = attribute.getValue();
            // It might not be an id reference, so check manually rather than just
            // calling stripIdPrefix():
            if (id.startsWith(NEW_ID_PREFIX)) {
                return id.substring(NEW_ID_PREFIX.length());
            } else if (id.startsWith(ID_PREFIX)) {
                return id.substring(ID_PREFIX.length());
            }
        }

        return null;
    }

    /**
     * Updates the constraints in the layout to handle deletion of a set of
     * nodes. This ensures that any constraints pointing to one of the deleted
     * nodes are changed properly to point to a non-deleted node with similar
     * constraints.
     */
    public void updateConstraints() {
        if (mChildren.length == mDeleted.size()) {
            // Deleting everything: Nothing to be done
            return;
        }

        // Now remove incoming edges to any views that were deleted. If possible,
        // don't just delete them but replace them with a transitive constraint, e.g.
        // if we have "A <= B <= C" and "B" is removed, then we end up with "A <= C",

        for (INode child : mChildren) {
            if (mDeleted.contains(child)) {
                continue;
            }

            for (IAttribute attribute : child.getLiveAttributes()) {
                String id = getId(attribute);
                if (id != null) {
                    if (mDeletedIds.contains(id)) {
                        // Unset this reference to a deleted widget. It might be
                        // replaced if the pointed to node points to some other node
                        // on the same side, but it may use a different constraint name,
                        // or have none at all (e.g. parent).
                        String name = attribute.getName();
                        child.setAttribute(ANDROID_URI, name, null);

                        INode deleted = mNodeMap.get(id);
                        if (deleted != null) {
                            ConstraintType type = ConstraintType.fromAttribute(name);
                            if (type != null) {
                                transfer(deleted, child, type, 0);
                            }
                        }
                    }
                }
            }
        }
    }

    private void transfer(INode deleted, INode target, ConstraintType targetType, int depth) {
        if (depth == 20) {
            // Prevent really deep flow or unbounded recursion in case there is a bug in
            // the cycle detection code
            return;
        }

        assert mDeleted.contains(deleted);

        for (IAttribute attribute : deleted.getLiveAttributes()) {
            String name = attribute.getName();
            ConstraintType type = ConstraintType.fromAttribute(name);
            if (type == null) {
                continue;
            }

            ConstraintType transfer = getCompatibleConstraint(type, targetType);
            if (transfer != null) {
                String id = getId(attribute);
                if (id != null) {
                    if (mDeletedIds.contains(id)) {
                        INode nextDeleted = mNodeMap.get(id);
                        if (nextDeleted != null) {
                            // Points to another deleted node: recurse
                            transfer(nextDeleted, target, targetType, depth + 1);
                        }
                    } else {
                        // Found an undeleted node destination: point to it directly.
                        // Note that we're using the
                        target.setAttribute(ANDROID_URI, transfer.name, attribute.getValue());
                    }
                } else {
                    // Pointing to parent or center etc (non-id ref): replicate this on the target
                    target.setAttribute(ANDROID_URI, name, attribute.getValue());
                }
            }
        }
    }

    /**
     * Determines if two constraints are in the same direction and if so returns
     * the constraint in the same direction. Rather than returning boolean true
     * or false, this returns the constraint which is sometimes modified. For
     * example, if you have a node which points left to a node which is centered
     * in parent, then the constraint is turned into center horizontal.
     */
    @Nullable
    private static ConstraintType getCompatibleConstraint(
            @NonNull ConstraintType first, @NonNull ConstraintType second) {
        if (first == second) {
            return first;
        }

        switch (second) {
            case ALIGN_LEFT:
            case LAYOUT_RIGHT_OF:
                switch (first) {
                    case LAYOUT_CENTER_HORIZONTAL:
                    case LAYOUT_LEFT_OF:
                    case ALIGN_LEFT:
                        return first;
                    case LAYOUT_CENTER_IN_PARENT:
                        return LAYOUT_CENTER_HORIZONTAL;
                }
                return null;

            case ALIGN_RIGHT:
            case LAYOUT_LEFT_OF:
                switch (first) {
                    case LAYOUT_CENTER_HORIZONTAL:
                    case ALIGN_RIGHT:
                    case LAYOUT_LEFT_OF:
                        return first;
                    case LAYOUT_CENTER_IN_PARENT:
                        return LAYOUT_CENTER_HORIZONTAL;
                }
                return null;

            case ALIGN_TOP:
            case LAYOUT_BELOW:
            case ALIGN_BASELINE:
                switch (first) {
                    case LAYOUT_CENTER_VERTICAL:
                    case ALIGN_TOP:
                    case LAYOUT_BELOW:
                    case ALIGN_BASELINE:
                        return first;
                    case LAYOUT_CENTER_IN_PARENT:
                        return LAYOUT_CENTER_VERTICAL;
                }
                return null;
            case ALIGN_BOTTOM:
            case LAYOUT_ABOVE:
                switch (first) {
                    case LAYOUT_CENTER_VERTICAL:
                    case ALIGN_BOTTOM:
                    case LAYOUT_ABOVE:
                    case ALIGN_BASELINE:
                        return first;
                    case LAYOUT_CENTER_IN_PARENT:
                        return LAYOUT_CENTER_VERTICAL;
                }
                return null;
        }

        return null;
    }
}