| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 |
| * |
| * 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.intellij.android.designer.model.layout.relative; |
| |
| import com.android.SdkConstants; |
| import com.android.tools.lint.detector.api.LintUtils; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| import com.intellij.android.designer.model.RadViewComponent; |
| import com.intellij.designer.model.RadComponent; |
| import com.intellij.psi.xml.XmlAttribute; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import static com.android.SdkConstants.*; |
| import static com.intellij.android.designer.model.layout.relative.ConstraintType.LAYOUT_CENTER_HORIZONTAL; |
| import static com.intellij.android.designer.model.layout.relative.ConstraintType.LAYOUT_CENTER_VERTICAL; |
| |
| /** |
| * 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 List<RadComponent> myChildren; |
| private final List<RadViewComponent> myDeleted; |
| private final Set<String> myDeletedIds; |
| private final Map<String, RadViewComponent> myNodeMap; |
| |
| /** |
| * 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(@NotNull List<RadViewComponent> deleted, |
| @NotNull List<RadViewComponent> moved, |
| @NotNull RadViewComponent layout) { |
| myDeleted = deleted; |
| myChildren = layout.getChildren(); |
| myNodeMap = Maps.newHashMapWithExpectedSize(myChildren.size()); |
| for (RadViewComponent view : RadViewComponent.getViewComponents(myChildren)) { |
| String id = view.getId(); |
| if (id != null) { |
| myNodeMap.put(LintUtils.stripIdPrefix(id), view); |
| } |
| } |
| |
| myDeletedIds = Sets.newHashSetWithExpectedSize(myDeleted.size()); |
| for (RadViewComponent node : myDeleted) { |
| String id = node.getId(); |
| if (id != null) { |
| myDeletedIds.add(LintUtils.stripIdPrefix(id)); |
| } |
| } |
| |
| // Any widgets that remain (e.g. typically because they were moved) should |
| // keep their incoming dependencies |
| for (RadViewComponent node : moved) { |
| String id = node.getId(); |
| if (id != null) { |
| myDeletedIds.remove(LintUtils.stripIdPrefix(id)); |
| } |
| } |
| } |
| |
| @Nullable |
| private static String getId(@NotNull XmlAttribute attribute) { |
| if (attribute.getLocalName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) && |
| ANDROID_URI.equals(attribute.getNamespace()) && |
| !attribute.getLocalName().startsWith(ATTR_LAYOUT_MARGIN)) { |
| String id = attribute.getValue(); |
| if (id == null) { |
| return null; |
| } |
| // 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 (myChildren.size() == myDeleted.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 (RadViewComponent child : RadViewComponent.getViewComponents(myChildren)) { |
| if (myDeleted.contains(child)) { |
| continue; |
| } |
| |
| for (XmlAttribute attribute : child.getTag().getAttributes()) { |
| String id = getId(attribute); |
| if (id != null) { |
| if (myDeletedIds.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.getLocalName(); |
| attribute.delete(); |
| |
| RadViewComponent deleted = myNodeMap.get(id); |
| if (deleted != null) { |
| ConstraintType type = ConstraintType.fromAttribute(name); |
| if (type != null) { |
| transfer(deleted, child, type, 0); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| private void transfer(RadViewComponent deleted, RadViewComponent 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; |
| } |
| |
| if (!myDeleted.contains(deleted)) { |
| return; |
| } |
| |
| for (XmlAttribute attribute : deleted.getTag().getAttributes()) { |
| String name = attribute.getLocalName(); |
| 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 (myDeletedIds.contains(id)) { |
| RadViewComponent nextDeleted = myNodeMap.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.getTag().setAttribute(transfer.name, ANDROID_URI, attribute.getValue()); |
| } |
| } |
| else { |
| // Pointing to parent or center etc (non-id ref): replicate this on the target |
| target.getTag().setAttribute(name, ANDROID_URI, 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(@NotNull ConstraintType first, @NotNull 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; |
| } |
| } |