blob: 32e3b6443270ad998d5184d6267638dc3599276b [file] [log] [blame]
/*
* 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;
}
}