blob: 92ccf2e7d31f03b634b4bf095d8e18393293f9da [file] [log] [blame]
/*
* Copyright (C) 2008 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.android.ide.eclipse.adt.internal.editors.ui.tree;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
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.uimodel.UiDocumentNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.swt.widgets.Shell;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import java.util.List;
/**
* Performs basic actions on an XML tree: add node, remove node, move up/down.
*/
public abstract class UiActions implements ICommitXml {
public UiActions() {
}
//---------------------
// Actual implementations must override these to provide specific hooks
/** Returns the UiDocumentNode for the current model. */
abstract protected UiElementNode getRootNode();
/** Commits pending data before the XML model is modified. */
@Override
abstract public void commitPendingXmlChanges();
/**
* Utility method to select an outline item based on its model node
*
* @param uiNode The node to select. Can be null (in which case nothing should happen)
*/
abstract protected void selectUiNode(UiElementNode uiNode);
//---------------------
/**
* Called when the "Add..." button next to the tree view is selected.
* <p/>
* This simplified version of doAdd does not support descriptor filters and creates
* a new {@link UiModelTreeLabelProvider} for each call.
*/
public void doAdd(UiElementNode uiNode, Shell shell) {
doAdd(uiNode, null /* descriptorFilters */, shell, new UiModelTreeLabelProvider());
}
/**
* Called when the "Add..." button next to the tree view is selected.
*
* Displays a selection dialog that lets the user select which kind of node
* to create, depending on the current selection.
*/
public void doAdd(UiElementNode uiNode,
ElementDescriptor[] descriptorFilters,
Shell shell, ILabelProvider labelProvider) {
// If the root node is a document with already a root, use it as the root node
UiElementNode rootNode = getRootNode();
if (rootNode instanceof UiDocumentNode && rootNode.getUiChildren().size() > 0) {
rootNode = rootNode.getUiChildren().get(0);
}
NewItemSelectionDialog dlg = new NewItemSelectionDialog(
shell,
labelProvider,
descriptorFilters,
uiNode, rootNode);
dlg.open();
Object[] results = dlg.getResult();
if (results != null && results.length > 0) {
addElement(dlg.getChosenRootNode(), null, (ElementDescriptor) results[0],
true /*updateLayout*/);
}
}
/**
* Adds a new XML element based on the {@link ElementDescriptor} to the given parent
* {@link UiElementNode}, and then select it.
* <p/>
* If the parent is a document root which already contains a root element, the inner
* root element is used as the actual parent. This ensure you can't create a broken
* XML file with more than one root element.
* <p/>
* If a sibling is given and that sibling has the same parent, the new node is added
* right after that sibling. Otherwise the new node is added at the end of the parent
* child list.
*
* @param uiParent An existing UI node or null to add to the tree root
* @param uiSibling An existing UI node before which to insert the new node. Can be null.
* @param descriptor The descriptor of the element to add
* @param updateLayout True if layout attributes should be set
* @return The new {@link UiElementNode} or null.
*/
public UiElementNode addElement(UiElementNode uiParent,
UiElementNode uiSibling,
ElementDescriptor descriptor,
boolean updateLayout) {
if (uiParent instanceof UiDocumentNode && uiParent.getUiChildren().size() > 0) {
uiParent = uiParent.getUiChildren().get(0);
}
if (uiSibling != null && uiSibling.getUiParent() != uiParent) {
uiSibling = null;
}
UiElementNode uiNew = addNewTreeElement(uiParent, uiSibling, descriptor, updateLayout);
selectUiNode(uiNew);
return uiNew;
}
/**
* Called when the "Remove" button is selected.
*
* If the tree has a selection, remove it.
* This simply deletes the XML node attached to the UI node: when the XML model fires the
* update event, the tree will get refreshed.
*/
public void doRemove(final List<UiElementNode> nodes, Shell shell) {
if (nodes == null || nodes.size() == 0) {
return;
}
final int len = nodes.size();
StringBuilder sb = new StringBuilder();
for (UiElementNode node : nodes) {
sb.append("\n- "); //$NON-NLS-1$
sb.append(node.getBreadcrumbTrailDescription(false /* include_root */));
}
if (MessageDialog.openQuestion(shell,
len > 1 ? "Remove elements from Android XML" // title
: "Remove element from Android XML",
String.format("Do you really want to remove %1$s?", sb.toString()))) {
commitPendingXmlChanges();
getRootNode().getEditor().wrapEditXmlModel(new Runnable() {
@Override
public void run() {
UiElementNode previous = null;
UiElementNode parent = null;
for (int i = len - 1; i >= 0; i--) {
UiElementNode node = nodes.get(i);
previous = node.getUiPreviousSibling();
parent = node.getUiParent();
// delete node
node.deleteXmlNode();
}
// try to select the last previous sibling or the last parent
if (previous != null) {
selectUiNode(previous);
} else if (parent != null) {
selectUiNode(parent);
}
}
});
}
}
/**
* Called when the "Up" button is selected.
* <p/>
* If the tree has a selection, move it up, either in the child list or as the last child
* of the previous parent.
*/
public void doUp(
final List<UiElementNode> uiNodes,
final ElementDescriptor[] descriptorFilters) {
if (uiNodes == null || uiNodes.size() < 1) {
return;
}
final Node[] selectXmlNode = { null };
final UiElementNode[] uiLastNode = { null };
final UiElementNode[] uiSearchRoot = { null };
commitPendingXmlChanges();
getRootNode().getEditor().wrapEditXmlModel(new Runnable() {
@Override
public void run() {
for (int i = 0; i < uiNodes.size(); i++) {
UiElementNode uiNode = uiLastNode[0] = uiNodes.get(i);
doUpInternal(
uiNode,
descriptorFilters,
selectXmlNode,
uiSearchRoot,
false /*testOnly*/);
}
}
});
assert uiLastNode[0] != null; // tell Eclipse this can't be null below
if (selectXmlNode[0] == null) {
// The XML node has not been moved, we can just select the same UI node
selectUiNode(uiLastNode[0]);
} else {
// The XML node has moved. At this point the UI model has been reloaded
// and the XML node has been affected to a new UI node. Find that new UI
// node and select it.
if (uiSearchRoot[0] == null) {
uiSearchRoot[0] = uiLastNode[0].getUiRoot();
}
if (uiSearchRoot[0] != null) {
selectUiNode(uiSearchRoot[0].findXmlNode(selectXmlNode[0]));
}
}
}
/**
* Checks whether the "up" action can be performed on all items.
*
* @return True if the up action can be carried on *all* items.
*/
public boolean canDoUp(
List<UiElementNode> uiNodes,
ElementDescriptor[] descriptorFilters) {
if (uiNodes == null || uiNodes.size() < 1) {
return false;
}
final Node[] selectXmlNode = { null };
final UiElementNode[] uiSearchRoot = { null };
commitPendingXmlChanges();
for (int i = 0; i < uiNodes.size(); i++) {
if (!doUpInternal(
uiNodes.get(i),
descriptorFilters,
selectXmlNode,
uiSearchRoot,
true /*testOnly*/)) {
return false;
}
}
return true;
}
private boolean doUpInternal(
UiElementNode uiNode,
ElementDescriptor[] descriptorFilters,
Node[] outSelectXmlNode,
UiElementNode[] outUiSearchRoot,
boolean testOnly) {
// the node will move either up to its parent or grand-parent
outUiSearchRoot[0] = uiNode.getUiParent();
if (outUiSearchRoot[0] != null && outUiSearchRoot[0].getUiParent() != null) {
outUiSearchRoot[0] = outUiSearchRoot[0].getUiParent();
}
Node xmlNode = uiNode.getXmlNode();
ElementDescriptor nodeDesc = uiNode.getDescriptor();
if (xmlNode == null || nodeDesc == null) {
return false;
}
UiElementNode uiParentNode = uiNode.getUiParent();
Node xmlParent = uiParentNode == null ? null : uiParentNode.getXmlNode();
if (xmlParent == null) {
return false;
}
UiElementNode uiPrev = uiNode.getUiPreviousSibling();
// Only accept a sibling that has an XML attached and
// is part of the allowed descriptor filters.
while (uiPrev != null &&
(uiPrev.getXmlNode() == null || !matchDescFilter(descriptorFilters, uiPrev))) {
uiPrev = uiPrev.getUiPreviousSibling();
}
if (uiPrev != null && uiPrev.getXmlNode() != null) {
// This node is not the first one of the parent.
Node xmlPrev = uiPrev.getXmlNode();
if (uiPrev.getDescriptor().acceptChild(nodeDesc)) {
// If the previous sibling can accept this child, then it
// is inserted at the end of the children list.
if (testOnly) {
return true;
}
xmlPrev.appendChild(xmlParent.removeChild(xmlNode));
outSelectXmlNode[0] = xmlNode;
} else {
// This node is not the first one of the parent, so it can be
// removed and then inserted before its previous sibling.
if (testOnly) {
return true;
}
xmlParent.insertBefore(
xmlParent.removeChild(xmlNode),
xmlPrev);
outSelectXmlNode[0] = xmlNode;
}
} else if (uiParentNode != null && !(xmlParent instanceof Document)) {
UiElementNode uiGrandParent = uiParentNode.getUiParent();
Node xmlGrandParent = uiGrandParent == null ? null : uiGrandParent.getXmlNode();
ElementDescriptor grandDesc =
uiGrandParent == null ? null : uiGrandParent.getDescriptor();
if (xmlGrandParent != null &&
!(xmlGrandParent instanceof Document) &&
grandDesc != null &&
grandDesc.acceptChild(nodeDesc)) {
// If the node is the first one of the child list of its
// parent, move it up in the hierarchy as previous sibling
// to the parent. This is only possible if the parent of the
// parent is not a document.
// The parent node must actually accept this kind of child.
if (testOnly) {
return true;
}
xmlGrandParent.insertBefore(
xmlParent.removeChild(xmlNode),
xmlParent);
outSelectXmlNode[0] = xmlNode;
}
}
return false;
}
private boolean matchDescFilter(
ElementDescriptor[] descriptorFilters,
UiElementNode uiNode) {
if (descriptorFilters == null || descriptorFilters.length < 1) {
return true;
}
ElementDescriptor desc = uiNode.getDescriptor();
for (ElementDescriptor filter : descriptorFilters) {
if (filter.equals(desc)) {
return true;
}
}
return false;
}
/**
* Called when the "Down" button is selected.
*
* If the tree has a selection, move it down, either in the same child list or as the
* first child of the next parent.
*/
public void doDown(
final List<UiElementNode> nodes,
final ElementDescriptor[] descriptorFilters) {
if (nodes == null || nodes.size() < 1) {
return;
}
final Node[] selectXmlNode = { null };
final UiElementNode[] uiLastNode = { null };
final UiElementNode[] uiSearchRoot = { null };
commitPendingXmlChanges();
getRootNode().getEditor().wrapEditXmlModel(new Runnable() {
@Override
public void run() {
for (int i = nodes.size() - 1; i >= 0; i--) {
final UiElementNode node = uiLastNode[0] = nodes.get(i);
doDownInternal(
node,
descriptorFilters,
selectXmlNode,
uiSearchRoot,
false /*testOnly*/);
}
}
});
assert uiLastNode[0] != null; // tell Eclipse this can't be null below
if (selectXmlNode[0] == null) {
// The XML node has not been moved, we can just select the same UI node
selectUiNode(uiLastNode[0]);
} else {
// The XML node has moved. At this point the UI model has been reloaded
// and the XML node has been affected to a new UI node. Find that new UI
// node and select it.
if (uiSearchRoot[0] == null) {
uiSearchRoot[0] = uiLastNode[0].getUiRoot();
}
if (uiSearchRoot[0] != null) {
selectUiNode(uiSearchRoot[0].findXmlNode(selectXmlNode[0]));
}
}
}
/**
* Checks whether the "down" action can be performed on all items.
*
* @return True if the down action can be carried on *all* items.
*/
public boolean canDoDown(
List<UiElementNode> uiNodes,
ElementDescriptor[] descriptorFilters) {
if (uiNodes == null || uiNodes.size() < 1) {
return false;
}
final Node[] selectXmlNode = { null };
final UiElementNode[] uiSearchRoot = { null };
commitPendingXmlChanges();
for (int i = 0; i < uiNodes.size(); i++) {
if (!doDownInternal(
uiNodes.get(i),
descriptorFilters,
selectXmlNode,
uiSearchRoot,
true /*testOnly*/)) {
return false;
}
}
return true;
}
private boolean doDownInternal(
UiElementNode uiNode,
ElementDescriptor[] descriptorFilters,
Node[] outSelectXmlNode,
UiElementNode[] outUiSearchRoot,
boolean testOnly) {
// the node will move either down to its parent or grand-parent
outUiSearchRoot[0] = uiNode.getUiParent();
if (outUiSearchRoot[0] != null && outUiSearchRoot[0].getUiParent() != null) {
outUiSearchRoot[0] = outUiSearchRoot[0].getUiParent();
}
Node xmlNode = uiNode.getXmlNode();
ElementDescriptor nodeDesc = uiNode.getDescriptor();
if (xmlNode == null || nodeDesc == null) {
return false;
}
UiElementNode uiParentNode = uiNode.getUiParent();
Node xmlParent = uiParentNode == null ? null : uiParentNode.getXmlNode();
if (xmlParent == null) {
return false;
}
UiElementNode uiNext = uiNode.getUiNextSibling();
// Only accept a sibling that has an XML attached and
// is part of the allowed descriptor filters.
while (uiNext != null &&
(uiNext.getXmlNode() == null || !matchDescFilter(descriptorFilters, uiNext))) {
uiNext = uiNext.getUiNextSibling();
}
if (uiNext != null && uiNext.getXmlNode() != null) {
// This node is not the last one of the parent.
Node xmlNext = uiNext.getXmlNode();
// If the next sibling is a node that can have children, though,
// then the node is inserted as the first child.
if (uiNext.getDescriptor().acceptChild(nodeDesc)) {
if (testOnly) {
return true;
}
// Note: insertBefore works as append if the ref node is
// null, i.e. when the node doesn't have children yet.
xmlNext.insertBefore(
xmlParent.removeChild(xmlNode),
xmlNext.getFirstChild());
outSelectXmlNode[0] = xmlNode;
} else {
// This node is not the last one of the parent, so it can be
// removed and then inserted after its next sibling.
if (testOnly) {
return true;
}
// Insert "before after next" ;-)
xmlParent.insertBefore(
xmlParent.removeChild(xmlNode),
xmlNext.getNextSibling());
outSelectXmlNode[0] = xmlNode;
}
} else if (uiParentNode != null && !(xmlParent instanceof Document)) {
UiElementNode uiGrandParent = uiParentNode.getUiParent();
Node xmlGrandParent = uiGrandParent == null ? null : uiGrandParent.getXmlNode();
ElementDescriptor grandDesc =
uiGrandParent == null ? null : uiGrandParent.getDescriptor();
if (xmlGrandParent != null &&
!(xmlGrandParent instanceof Document) &&
grandDesc != null &&
grandDesc.acceptChild(nodeDesc)) {
// This node is the last node of its parent.
// If neither the parent nor the grandparent is a document,
// then the node can be inserted right after the parent.
// The parent node must actually accept this kind of child.
if (testOnly) {
return true;
}
xmlGrandParent.insertBefore(
xmlParent.removeChild(xmlNode),
xmlParent.getNextSibling());
outSelectXmlNode[0] = xmlNode;
}
}
return false;
}
//---------------------
/**
* Adds a new element of the given descriptor's type to the given UI parent node.
*
* This actually creates the corresponding XML node in the XML model, which in turn
* will refresh the current tree view.
*
* @param uiParent An existing UI node or null to add to the tree root
* @param uiSibling An existing UI node to insert right before. Can be null.
* @param descriptor The descriptor of the element to add
* @param updateLayout True if layout attributes should be set
* @return The {@link UiElementNode} that has been added to the UI tree.
*/
private UiElementNode addNewTreeElement(UiElementNode uiParent,
UiElementNode uiSibling,
ElementDescriptor descriptor,
final boolean updateLayout) {
commitPendingXmlChanges();
List<UiElementNode> uiChildren = uiParent.getUiChildren();
int n = uiChildren.size();
// The default is to append at the end of the list.
int index = n;
if (uiSibling != null) {
// Try to find the requested sibling.
index = uiChildren.indexOf(uiSibling);
if (index < 0) {
// This sibling didn't exist. Should not happen but compensate
// by simply adding to the end of the list.
uiSibling = null;
index = n;
}
}
if (uiSibling == null) {
// If we don't require any specific position, make sure to insert before the
// first mandatory_last descriptor's position, if any.
for (int i = 0; i < n; i++) {
UiElementNode uiChild = uiChildren.get(i);
if (uiChild.getDescriptor().getMandatory() == Mandatory.MANDATORY_LAST) {
index = i;
break;
}
}
}
final UiElementNode uiNew = uiParent.insertNewUiChild(index, descriptor);
UiElementNode rootNode = getRootNode();
rootNode.getEditor().wrapEditXmlModel(new Runnable() {
@Override
public void run() {
DescriptorsUtils.setDefaultLayoutAttributes(uiNew, updateLayout);
uiNew.createXmlNode();
}
});
return uiNew;
}
}