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