| /* |
| * 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; |
| } |
| } |