import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IEditorInput;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.ui.StructuredTextEditor;
+import org.eclipse.wst.sse.ui.internal.StructuredTextViewer;
import org.eclipse.wst.xml.core.internal.document.NodeContainer;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.w3c.dom.Document;
public abstract class AndroidXmlEditor extends FormEditor implements IResourceChangeListener {
/** Preference name for the current page of this file */
- private static final String PREF_CURRENT_PAGE = "_current_page";
+ private static final String PREF_CURRENT_PAGE = "_current_page"; // $NON-NLS-1$
/** Id string used to create the Android SDK browser */
private static String BROWSER_ID = "android"; // $NON-NLS-1$
* Get the XML text directly from the editor.
*
* @param xmlNode The node whose XML text we want to obtain.
- * @return The XML representation of the {@link Node}.
+ * @return The XML representation of the {@link Node}, or null if there was an error.
*/
public String getXmlText(Node xmlNode) {
String data = null;
}
/**
+ * Formats the text around the given caret range, using the current Eclipse
+ * XML formatter settings.
+ *
+ * @param begin The starting offset of the range to be reformatted.
+ * @param end The ending offset of the range to be reformatted.
+ */
+ public void reformatRegion(int begin, int end) {
+ ISourceViewer textViewer = getStructuredSourceViewer();
+
+ // Clamp text range to valid offsets.
+ IDocument document = textViewer.getDocument();
+ int documentLength = document.getLength();
+ end = Math.min(end, documentLength);
+ begin = Math.min(begin, end);
+
+ // It turns out the XML formatter does *NOT* format things correctly if you
+ // select just a region of text. You *MUST* also include the leading whitespace
+ // on the line, or it will dedent all the content to column 0. Therefore,
+ // we must figure out the offset of the start of the line that contains the
+ // beginning of the tag.
+ try {
+ IRegion lineInformation = document.getLineInformationOfOffset(begin);
+ if (lineInformation != null) {
+ int lineBegin = lineInformation.getOffset();
+ if (lineBegin != begin) {
+ begin = lineBegin;
+ } else if (begin > 0) {
+ // Trick #2: It turns out that, if an XML element starts in column 0,
+ // then the XML formatter will NOT indent it (even if its parent is
+ // indented). If you on the other hand include the end of the previous
+ // line (the newline), THEN the formatter also correctly inserts the
+ // element. Therefore, we adjust the beginning range to include the
+ // previous line (if we are not already in column 0 of the first line)
+ // in the case where the element starts the line.
+ begin--;
+ }
+ }
+ } catch (BadLocationException e) {
+ // This cannot happen because we already clamped the offsets
+ AdtPlugin.log(e, e.toString()); // $NON-NLS-1$
+ }
+
+ if (textViewer instanceof StructuredTextViewer) {
+ StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer;
+ int operation = ISourceViewer.FORMAT;
+ boolean canFormat = structuredTextViewer.canDoOperation(operation);
+ if (canFormat) {
+ StyledText textWidget = textViewer.getTextWidget();
+ textWidget.setSelection(begin, end);
+ structuredTextViewer.doOperation(operation);
+ }
+ }
+ }
+
+ /**
+ * Formats the XML region corresponding to the given node.
+ *
+ * @param node The node to be formatted.
+ */
+ public void reformatNode(Node node) {
+ if (mIsCreatingPage) {
+ return;
+ }
+
+ if (node instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) node;
+ int begin = region.getStartOffset();
+ int end = region.getEndOffset();
+ reformatRegion(begin, end);
+ }
+ }
+
+ /**
+ * Formats the XML document according to the user's XML formatting settings.
+ */
+ public void reformatDocument() {
+ ISourceViewer textViewer = getStructuredSourceViewer();
+ if (textViewer instanceof StructuredTextViewer) {
+ StructuredTextViewer structuredTextViewer = (StructuredTextViewer) textViewer;
+ int operation = StructuredTextViewer.FORMAT_DOCUMENT;
+ boolean canFormat = structuredTextViewer.canDoOperation(operation);
+ if (canFormat) {
+ structuredTextViewer.doOperation(operation);
+ }
+ }
+ }
+
+ /**
+ * Returns the indentation String of the given node.
+ *
+ * @param xmlNode The node whose indentation we want.
+ * @return The indent-string of the given node, or "" if the indentation for some reason could
+ * not be computed.
+ */
+ public String getIndent(Node xmlNode) {
+ assert xmlNode.getNodeType() == Node.ELEMENT_NODE;
+
+ if (xmlNode instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion)xmlNode;
+ IDocument document = getStructuredSourceViewer().getDocument();
+ int startOffset = region.getStartOffset();
+ try {
+ IRegion lineInformation = document.getLineInformationOfOffset(startOffset);
+ if (lineInformation != null) {
+ int lineBegin = lineInformation.getOffset();
+ if (lineBegin != startOffset) {
+ String prefix = document.get(lineBegin, startOffset - lineBegin);
+
+ // It's possible that the tag whose indentation we seek is not
+ // at the beginning of the line. In that case we'll just return
+ // the indentation of the line itself.
+ for (int i = 0; i < prefix.length(); i++) {
+ if (!Character.isWhitespace(prefix.charAt(i))) {
+ return prefix.substring(0, i);
+ }
+ }
+
+ return prefix;
+ }
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, "Could not obtain indentation"); // $NON-NLS-1$
+ }
+ }
+
+ return ""; // $NON-NLS-1$
+ }
+
+ /**
* Listen to changes in the underlying XML model in the structured editor.
*/
private class XmlModelStateListener implements IModelStateListener {
import com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors;
import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener.UiUpdateState;
import com.android.ide.eclipse.adt.internal.editors.xml.descriptors.XmlDescriptors;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.ide.eclipse.adt.internal.resources.AttributeInfo;
import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
import com.android.sdklib.SdkConstants;
* <p/>
* The XML {@link Document} is initially null. The XML {@link Document} must be set only on the
* UI root element node (this method takes care of that.)
+ * @param xmlDoc The new XML document to associate this node with.
*/
public void setXmlDocument(Document xmlDoc) {
if (mUiParent == null) {
* <p/>
* Do not use this to call getDescriptor().getAttributes(), instead call
* getAttributeDescriptors() which can be overridden by derived classes.
+ * @return The {@link ElementDescriptor} for this node. This is never null.
*/
public ElementDescriptor getDescriptor() {
return mDescriptor;
* <p/>
* Use this instead of getDescriptor().getAttributes() -- derived classes can override
* this to manipulate the attribute descriptor list depending on the current UI node.
+ * @return The {@link AttributeDescriptor} array for the descriptor of this node.
*/
public AttributeDescriptor[] getAttributeDescriptors() {
return mDescriptor.getAttributes();
}
/**
- * Returns The root {@link UiElementNode}.
+ * Returns the root {@link UiElementNode}.
+ *
+ * @return The root {@link UiElementNode}.
*/
public UiElementNode getUiRoot() {
UiElementNode root = this;
}
/**
- * Returns the previous UI sibling of this UI node.
- * If the node does not have a previous sibling, returns null.
+ * Returns the previous UI sibling of this UI node. If the node does not have a previous
+ * sibling, returns null.
+ *
+ * @return The previous UI sibling of this UI node, or null if not applicable.
*/
public UiElementNode getUiPreviousSibling() {
if (mUiParent != null) {
/**
* Returns the next UI sibling of this UI node.
* If the node does not have a next sibling, returns null.
+ *
+ * @return The next UI sibling of this UI node, or null.
*/
public UiElementNode getUiNextSibling() {
if (mUiParent != null) {
* Sets the {@link AndroidXmlEditor} handling this {@link UiElementNode} hierarchy.
* <p/>
* The editor must always be set on the root node. This method takes care of that.
+ *
+ * @param editor The editor to associate this node with.
*/
public void setEditor(AndroidXmlEditor editor) {
if (mUiParent == null) {
/**
* Returns the Android target data for the file being edited.
+ *
+ * @return The Android target data for the file being edited.
*/
public AndroidTargetData getAndroidTarget() {
return getEditor().getTargetData();
/**
* Sets the error flag value.
+ *
* @param errorFlag the error flag
*/
public final void setHasError(boolean errorFlag) {
/**
* Returns whether this node, its attributes, or one of the children nodes (and attributes)
* has errors.
+ *
+ * @return True if this node, its attributes, or one of the children nodes (and attributes)
+ * has errors.
*/
public final boolean hasError() {
if (mHasError) {
/**
* Adds a new {@link IUiUpdateListener} to the internal update listener list.
+ *
+ * @param listener The listener to add.
*/
public void addUpdateListener(IUiUpdateListener listener) {
if (mUiUpdateListeners == null) {
/**
* Removes an existing {@link IUiUpdateListener} from the internal update listener list.
* Does nothing if the list is empty or the listener is not registered.
+ *
+ * @param listener The listener to remove.
*/
public void removeUpdateListener(IUiUpdateListener listener) {
if (mUiUpdateListeners != null) {
xmlNextSibling = uiNextSibling.getXmlNode();
}
- // If this is the first element we are adding into a new element,
- // we need to insert a newline at the beginning too. Unless it's already
- // there, which is the case for the root element created for the .xml template
- // files - but this is why we use the xml node list rather than the element
- // count.
- if (parentXmlNode.getChildNodes().getLength() == 0) {
- parentXmlNode.insertBefore(doc.createTextNode("\n"), xmlNextSibling); //$NON-NLS-1$
+ Node previousTextNode = null;
+ if (xmlNextSibling != null) {
+ Node previousNode = xmlNextSibling.getPreviousSibling();
+ if (previousNode != null && previousNode.getNodeType() == Node.TEXT_NODE) {
+ previousTextNode = previousNode;
+ }
+ } else {
+ Node lastChild = parentXmlNode.getLastChild();
+ if (lastChild != null && lastChild.getNodeType() == Node.TEXT_NODE) {
+ previousTextNode = lastChild;
+ }
}
+ String insertAfter = null;
+
+ // Try to figure out the indentation node to insert. Even in auto-formatting
+ // we need to do this, because it turns out the XML editor's formatter does
+ // not do a very good job with completely botched up XML; it does a much better
+ // job if the new XML is already mostly well formatted. Thus, the main purpose
+ // of applying the real XML formatter after our own indentation attempts here is
+ // to make it apply its own tab-versus-spaces indentation properties, have it
+ // insert line breaks before attributes (if the user has configured that), etc.
+
+ // First figure out the indentation level of the newly inserted element;
+ // this is either the same as the previous sibling, or if there is no sibling,
+ // it's the indentation of the parent plus one indentation level.
+ boolean isFirstChild = getUiPreviousSibling() == null
+ || parentXmlNode.getFirstChild() == null;
+ AndroidXmlEditor editor = getEditor();
+ String indent;
+ String parentIndent = ""; //$NON-NLS-1$
+ if (isFirstChild) {
+ indent = parentIndent = editor.getIndent(parentXmlNode);
+ // We need to add one level of indentation. Are we using tabs?
+ // Can't get to formatting settings so let's just look at the
+ // parent indentation and see if we can guess
+ if (indent.length() > 0 && indent.charAt(indent.length()-1) == '\t') {
+ indent = indent + '\t';
+ } else {
+ // Not using tabs, or we can't figure it out (because parent had no
+ // indentation). In that case, indent with 4 spaces, as seems to
+ // be the Android default.
+ indent = indent + " "; //$NON-NLS-1$
+ }
+ } else {
+ // Find out the indent of the previous sibling
+ indent = editor.getIndent(getUiPreviousSibling().getXmlNode());
+ }
+
+ // We want to insert the new element BEFORE the text node which precedes
+ // the next element, since that text node is the next element's indentation!
+ if (previousTextNode != null) {
+ xmlNextSibling = previousTextNode;
+ } else {
+ // If there's no previous text node, we are probably inside an
+ // empty element (<LinearLayout>|</LinearLayout>) and in that case we need
+ // to not only insert a newline and indentation before the new element, but
+ // after it as well.
+ insertAfter = parentIndent;
+ }
+
+ // Insert indent text node before the new element
+ Text indentNode = doc.createTextNode("\n" + indent); //$NON-NLS-1$
+ parentXmlNode.insertBefore(indentNode, xmlNextSibling);
+
+ // Insert the element itself
parentXmlNode.insertBefore(mXmlNode, xmlNextSibling);
- // Insert a separator after the tag, to make it easier to read
- Text sep = doc.createTextNode("\n"); //$NON-NLS-1$
- parentXmlNode.insertBefore(sep, xmlNextSibling);
+ // Insert a separator after the tag. We only do this when we've inserted
+ // a tag into an area where there was no whitespace before
+ // (e.g. a new child of <LinearLayout></LinearLayout>).
+ if (insertAfter != null) {
+ Text sep = doc.createTextNode("\n" + insertAfter); //$NON-NLS-1$
+ parentXmlNode.insertBefore(sep, xmlNextSibling);
+ }
// Set all initial attributes in the XML node if they are not empty.
// Iterate on the descriptor list to get the desired order and then use the
}
}
+ if (mUiParent != null) {
+ mUiParent.formatOnInsert(this);
+ }
+
invokeUiUpdateListeners(UiUpdateState.CREATED);
return mXmlNode;
}
if (xmlParent == null) {
xmlParent = getXmlDocument();
}
- Node nextSibling = oldXmlNode.getNextSibling();
+ Node previousSibling = oldXmlNode.getPreviousSibling();
oldXmlNode = xmlParent.removeChild(oldXmlNode);
- // Remove following text node if it's just blank space, to account for
- // the fact what we add these when we insert nodes.
- if (nextSibling != null && nextSibling.getNodeType() == Node.TEXT_NODE
- && nextSibling.getNodeValue().trim().length() == 0) {
- xmlParent.removeChild(nextSibling);
+ // We need to remove the text node BEFORE the removed element, since THAT's the
+ // indentation node for the removed element.
+ if (previousSibling != null && previousSibling.getNodeType() == Node.TEXT_NODE
+ && previousSibling.getNodeValue().trim().length() == 0) {
+ xmlParent.removeChild(previousSibling);
+ }
+
+ if (mUiParent != null) {
+ mUiParent.formatOnDeletion(this);
}
invokeUiUpdateListeners(UiUpdateState.DELETED);
}
mUiChildren.remove(uiIndex);
+
return true;
} finally {
// Tell listeners that a node has been removed.
});
}
}
+
+ /** Handles reformatting of the XML buffer when a given node has been inserted.
+ *
+ * @param node The node that was inserted.
+ */
+ private void formatOnInsert(UiElementNode node) {
+ // Reformat parent if it's the first child (such that it for example can force
+ // children into their own lines.)
+ if (mUiChildren.size() == 1) {
+ reformat();
+ } else {
+ // In theory, we should ONLY have to reformat the node itself:
+ // uiNode.reformat();
+ //
+ // However, the XML formatter does not correctly handle this; in particular
+ // it will -dedent- a correctly indented child. Here's an example:
+ //
+ // @formatter:off
+ // <?xml version="1.0" encoding="utf-8"?>
+ // <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ // android:layout_width="fill_parent" android:layout_height="fill_parent"
+ // android:orientation="vertical">
+ // <LinearLayout android:id="@+id/LinearLayout01"
+ // android:layout_width="wrap_content" android:layout_height="wrap_content">
+ // <Button android:id="@+id/Button03"></Button>
+ // </LinearLayout>
+ // </LinearLayout>
+ // @formatter:on
+ //
+ // If we have just inserted the button inside the nested LinearLayout, and
+ // attempt to format it, it will incorrectly dedent the button to be flush with
+ // its parent.
+ //
+ // Therefore, for now, in this case, format the PARENT on insert. This means that
+ // siblings can be formatted as well, but that can't be helped.
+
+ // This should be "uiNode.reformat();" instead of "reformat()" if formatting
+ // worked correctly:
+ reformat();
+ }
+ }
+
+ /**
+ * Handles reformatting of the XML buffer when a given node has been removed.
+ *
+ * @param node The node that was removed.
+ */
+ private void formatOnDeletion(UiElementNode node) {
+ // Reformat parent if it's the last child removed, such that we can for example
+ // place the closing element back on the same line as the opening tag (if the
+ // user has that mode configured in the formatting options.)
+ if (mUiChildren.size() <= 1) {
+ // <= 1 instead of == 0: turns out the parent hasn't always deleted
+ // this child from its its children list yet.
+ reformat();
+ }
+ }
+
+ /**
+ * Reformats the XML corresponding to the given XML node. This will do nothing if we have
+ * errors, or if the user has turned off XML auto-formatting.
+ */
+ private void reformat() {
+ if (mHasError || !AdtPrefs.getPrefs().getFormatXml()) {
+ return;
+ }
+
+ AndroidXmlEditor editor = getEditor();
+ if (editor != null && mXmlNode != null) {
+ editor.reformatNode(mXmlNode);
+ }
+ }
}