aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/EditableDialogCellEditor.java490
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java72
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/FlagValueCellEditor.java58
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ListValueCellEditor.java76
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ResourceValueCellEditor.java59
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/SectionHelper.java364
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/TextValueCellEditor.java43
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/UiElementPart.java284
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/CopyCutAction.java221
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/ICommitXml.java28
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/NewItemSelectionDialog.java415
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/PasteAction.java129
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiActions.java598
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiElementDetail.java494
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeContentProvider.java120
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeLabelProvider.java106
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiTreeBlock.java946
17 files changed, 4503 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/EditableDialogCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/EditableDialogCellEditor.java
new file mode 100644
index 000000000..baf8a1039
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/EditableDialogCellEditor.java
@@ -0,0 +1,490 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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;
+
+import com.android.SdkConstants;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.jface.viewers.DialogCellEditor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusAdapter;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.KeyAdapter;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.TraverseEvent;
+import org.eclipse.swt.events.TraverseListener;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Text;
+
+import java.text.MessageFormat;
+
+/**
+ * Custom DialogCellEditor, replacing the Label with an editable {@link Text} widget.
+ * <p/>Also set the button to {@link SWT#FLAT} to make sure it looks good on MacOS X.
+ * <p/>Most of the code comes from TextCellEditor.
+ */
+public abstract class EditableDialogCellEditor extends DialogCellEditor {
+
+ private Text text;
+
+ private ModifyListener modifyListener;
+
+ /**
+ * State information for updating action enablement
+ */
+ private boolean isSelection = false;
+
+ private boolean isDeleteable = false;
+
+ private boolean isSelectable = false;
+
+ EditableDialogCellEditor(Composite parent) {
+ super(parent);
+ }
+
+ /*
+ * Re-implement this method only to properly set the style in the button, or it won't look
+ * good in MacOS X
+ */
+ @Override
+ protected Button createButton(Composite parent) {
+ Button result = new Button(parent, SWT.DOWN | SWT.FLAT);
+ result.setText("..."); //$NON-NLS-1$
+ return result;
+ }
+
+
+ @Override
+ protected Control createContents(Composite cell) {
+ text = new Text(cell, SWT.SINGLE);
+ text.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ handleDefaultSelection(e);
+ }
+ });
+ text.addKeyListener(new KeyAdapter() {
+ // hook key pressed - see PR 14201
+ @Override
+ public void keyPressed(KeyEvent e) {
+ keyReleaseOccured(e);
+
+ // as a result of processing the above call, clients may have
+ // disposed this cell editor
+ if ((getControl() == null) || getControl().isDisposed()) {
+ return;
+ }
+ checkSelection(); // see explanation below
+ checkDeleteable();
+ checkSelectable();
+ }
+ });
+ text.addTraverseListener(new TraverseListener() {
+ @Override
+ public void keyTraversed(TraverseEvent e) {
+ if (e.detail == SWT.TRAVERSE_ESCAPE
+ || e.detail == SWT.TRAVERSE_RETURN) {
+ e.doit = false;
+ }
+ }
+ });
+ // We really want a selection listener but it is not supported so we
+ // use a key listener and a mouse listener to know when selection changes
+ // may have occurred
+ text.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseUp(MouseEvent e) {
+ checkSelection();
+ checkDeleteable();
+ checkSelectable();
+ }
+ });
+ text.addFocusListener(new FocusAdapter() {
+ @Override
+ public void focusLost(FocusEvent e) {
+ EditableDialogCellEditor.this.focusLost();
+ }
+ });
+ text.setFont(cell.getFont());
+ text.setBackground(cell.getBackground());
+ text.setText("");//$NON-NLS-1$
+ text.addModifyListener(getModifyListener());
+ return text;
+ }
+
+ /**
+ * Checks to see if the "deletable" state (can delete/
+ * nothing to delete) has changed and if so fire an
+ * enablement changed notification.
+ */
+ private void checkDeleteable() {
+ boolean oldIsDeleteable = isDeleteable;
+ isDeleteable = isDeleteEnabled();
+ if (oldIsDeleteable != isDeleteable) {
+ fireEnablementChanged(DELETE);
+ }
+ }
+
+ /**
+ * Checks to see if the "selectable" state (can select)
+ * has changed and if so fire an enablement changed notification.
+ */
+ private void checkSelectable() {
+ boolean oldIsSelectable = isSelectable;
+ isSelectable = isSelectAllEnabled();
+ if (oldIsSelectable != isSelectable) {
+ fireEnablementChanged(SELECT_ALL);
+ }
+ }
+
+ /**
+ * Checks to see if the selection state (selection /
+ * no selection) has changed and if so fire an
+ * enablement changed notification.
+ */
+ private void checkSelection() {
+ boolean oldIsSelection = isSelection;
+ isSelection = text.getSelectionCount() > 0;
+ if (oldIsSelection != isSelection) {
+ fireEnablementChanged(COPY);
+ fireEnablementChanged(CUT);
+ }
+ }
+
+ /* (non-Javadoc)
+ * Method declared on CellEditor.
+ */
+ @Override
+ protected void doSetFocus() {
+ if (text != null) {
+ text.selectAll();
+ text.setFocus();
+ checkSelection();
+ checkDeleteable();
+ checkSelectable();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.jface.viewers.DialogCellEditor#updateContents(java.lang.Object)
+ */
+ @Override
+ protected void updateContents(Object value) {
+ Assert.isTrue(text != null && (value == null || (value instanceof String)));
+ if (value != null) {
+ text.removeModifyListener(getModifyListener());
+ text.setText((String) value);
+ text.addModifyListener(getModifyListener());
+ }
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of
+ * this <code>CellEditor</code> framework method returns
+ * the text string.
+ *
+ * @return the text string
+ */
+ @Override
+ protected Object doGetValue() {
+ return text.getText();
+ }
+
+
+ /**
+ * Processes a modify event that occurred in this text cell editor.
+ * This framework method performs validation and sets the error message
+ * accordingly, and then reports a change via <code>fireEditorValueChanged</code>.
+ * Subclasses should call this method at appropriate times. Subclasses
+ * may extend or reimplement.
+ *
+ * @param e the SWT modify event
+ */
+ protected void editOccured(ModifyEvent e) {
+ String value = text.getText();
+ if (value == null) {
+ value = "";//$NON-NLS-1$
+ }
+ Object typedValue = value;
+ boolean oldValidState = isValueValid();
+ boolean newValidState = isCorrect(typedValue);
+
+ if (!newValidState) {
+ // try to insert the current value into the error message.
+ setErrorMessage(MessageFormat.format(getErrorMessage(),
+ new Object[] { value }));
+ }
+ valueChanged(oldValidState, newValidState);
+ }
+
+ /**
+ * Return the modify listener.
+ */
+ private ModifyListener getModifyListener() {
+ if (modifyListener == null) {
+ modifyListener = new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ editOccured(e);
+ }
+ };
+ }
+ return modifyListener;
+ }
+
+ /**
+ * Handles a default selection event from the text control by applying the editor
+ * value and deactivating this cell editor.
+ *
+ * @param event the selection event
+ *
+ * @since 3.0
+ */
+ protected void handleDefaultSelection(SelectionEvent event) {
+ // same with enter-key handling code in keyReleaseOccured(e);
+ fireApplyEditorValue();
+ deactivate();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method returns <code>true</code> if
+ * the current selection is not empty.
+ */
+ @Override
+ public boolean isCopyEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return text.getSelectionCount() > 0;
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method returns <code>true</code> if
+ * the current selection is not empty.
+ */
+ @Override
+ public boolean isCutEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return text.getSelectionCount() > 0;
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method returns <code>true</code>
+ * if there is a selection or if the caret is not positioned
+ * at the end of the text.
+ */
+ @Override
+ public boolean isDeleteEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return text.getSelectionCount() > 0
+ || text.getCaretPosition() < text.getCharCount();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method always returns <code>true</code>.
+ */
+ @Override
+ public boolean isPasteEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check if save all is enabled
+ * @return true if it is
+ */
+ public boolean isSaveAllEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns <code>true</code> if this cell editor is
+ * able to perform the select all action.
+ * <p>
+ * This default implementation always returns
+ * <code>false</code>.
+ * </p>
+ * <p>
+ * Subclasses may override
+ * </p>
+ * @return <code>true</code> if select all is possible,
+ * <code>false</code> otherwise
+ */
+ @Override
+ public boolean isSelectAllEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return text.getCharCount() > 0;
+ }
+
+ /**
+ * Processes a key release event that occurred in this cell editor.
+ * <p>
+ * The <code>TextCellEditor</code> implementation of this framework method
+ * ignores when the RETURN key is pressed since this is handled in
+ * <code>handleDefaultSelection</code>.
+ * An exception is made for Ctrl+Enter for multi-line texts, since
+ * a default selection event is not sent in this case.
+ * </p>
+ *
+ * @param keyEvent the key event
+ */
+ @Override
+ protected void keyReleaseOccured(KeyEvent keyEvent) {
+ if (keyEvent.character == '\r') { // Return key
+ // Enter is handled in handleDefaultSelection.
+ // Do not apply the editor value in response to an Enter key event
+ // since this can be received from the IME when the intent is -not-
+ // to apply the value.
+ // See bug 39074 [CellEditors] [DBCS] canna input mode fires bogus event from Text Control
+ //
+ // An exception is made for Ctrl+Enter for multi-line texts, since
+ // a default selection event is not sent in this case.
+ if (text != null && !text.isDisposed()
+ && (text.getStyle() & SWT.MULTI) != 0) {
+ if ((keyEvent.stateMask & SWT.CTRL) != 0) {
+ super.keyReleaseOccured(keyEvent);
+ }
+ }
+ return;
+ }
+ super.keyReleaseOccured(keyEvent);
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method copies the
+ * current selection to the clipboard.
+ */
+ @Override
+ public void performCopy() {
+ text.copy();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method cuts the
+ * current selection to the clipboard.
+ */
+ @Override
+ public void performCut() {
+ text.cut();
+ checkSelection();
+ checkDeleteable();
+ checkSelectable();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method deletes the
+ * current selection or, if there is no selection,
+ * the character next character from the current position.
+ */
+ @Override
+ public void performDelete() {
+ if (text.getSelectionCount() > 0) {
+ // remove the contents of the current selection
+ text.insert(""); //$NON-NLS-1$
+ } else {
+ // remove the next character
+ int pos = text.getCaretPosition();
+ if (pos < text.getCharCount()) {
+ text.setSelection(pos, pos + 1);
+ text.insert(""); //$NON-NLS-1$
+ }
+ }
+ checkSelection();
+ checkDeleteable();
+ checkSelectable();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method pastes the
+ * the clipboard contents over the current selection.
+ */
+ @Override
+ public void performPaste() {
+ text.paste();
+ checkSelection();
+ checkDeleteable();
+ checkSelectable();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method selects all of the
+ * current text.
+ */
+ @Override
+ public void performSelectAll() {
+ text.selectAll();
+ checkSelection();
+ checkDeleteable();
+ }
+
+ @Override
+ protected void focusLost() {
+ if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_LINUX) {
+ // On Linux, something about the order of focus event delivery prevents the
+ // callback on the "..." button to be invoked, which means the
+ // customizer dialog never shows up (see issue #18348).
+ // (Note that simply trying to Display.asyncRun() the super.focusLost()
+ // method does not work.)
+ //
+ // We can work around this by not deactivating on a focus loss.
+ // This means that in some cases the cell editor will still be
+ // shown in the property sheet, but I've tested that the values
+ // are all committed as before. This is better than having a non-operational
+ // customizer, but since this issue only happens on Linux the workaround
+ // is only done on Linux such that on other platforms we deactivate
+ // immediately on focus loss.
+ //
+ if (isActivated()) {
+ fireApplyEditorValue();
+ // super.focusLost calls the following which we're deliberately
+ // suppressing here:
+ // deactivate();
+ }
+ } else {
+ super.focusLost();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java
new file mode 100644
index 000000000..7085e5d50
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ErrorImageComposite.java
@@ -0,0 +1,72 @@
+package com.android.ide.eclipse.adt.internal.editors.ui;
+
+import static org.eclipse.ui.ISharedImages.IMG_DEC_FIELD_ERROR;
+import static org.eclipse.ui.ISharedImages.IMG_DEC_FIELD_WARNING;
+import static org.eclipse.ui.ISharedImages.IMG_OBJS_ERROR_TSK;
+import static org.eclipse.ui.ISharedImages.IMG_OBJS_WARN_TSK;
+
+import org.eclipse.jface.resource.CompositeImageDescriptor;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.DecorationOverlayIcon;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
+
+/**
+ * ImageDescriptor that adds a error marker.
+ * Based on {@link DecorationOverlayIcon} only available in Eclipse 3.3
+ */
+public class ErrorImageComposite extends CompositeImageDescriptor {
+
+ private Image mBaseImage;
+ private ImageDescriptor mErrorImageDescriptor;
+ private Point mSize;
+
+ /**
+ * Creates a new {@link ErrorImageComposite}
+ *
+ * @param baseImage the base image to overlay an icon on top of
+ */
+ public ErrorImageComposite(Image baseImage) {
+ this(baseImage, false);
+ }
+
+ /**
+ * Creates a new {@link ErrorImageComposite}
+ *
+ * @param baseImage the base image to overlay an icon on top of
+ * @param warning if true, add a warning icon, otherwise an error icon
+ */
+ public ErrorImageComposite(Image baseImage, boolean warning) {
+ mBaseImage = baseImage;
+ ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
+ mErrorImageDescriptor = sharedImages.getImageDescriptor(
+ warning ? IMG_DEC_FIELD_WARNING : IMG_DEC_FIELD_ERROR);
+ if (mErrorImageDescriptor == null) {
+ mErrorImageDescriptor = sharedImages.getImageDescriptor(
+ warning ? IMG_OBJS_WARN_TSK : IMG_OBJS_ERROR_TSK);
+ }
+ mSize = new Point(baseImage.getBounds().width, baseImage.getBounds().height);
+ }
+
+ @Override
+ protected void drawCompositeImage(int width, int height) {
+ ImageData baseData = mBaseImage.getImageData();
+ drawImage(baseData, 0, 0);
+
+ ImageData overlayData = mErrorImageDescriptor.getImageData();
+ if (overlayData.width == baseData.width && overlayData.height == baseData.height) {
+ overlayData = overlayData.scaledTo(14, 14);
+ drawImage(overlayData, -3, mSize.y - overlayData.height + 3);
+ } else {
+ drawImage(overlayData, 0, mSize.y - overlayData.height);
+ }
+ }
+
+ @Override
+ protected Point getSize() {
+ return mSize;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/FlagValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/FlagValueCellEditor.java
new file mode 100644
index 000000000..2a1bc36b5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/FlagValueCellEditor.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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;
+
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiFlagAttributeNode;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/**
+ * DialogCellEditor able to receive a {@link UiFlagAttributeNode} in the {@link #setValue(Object)}
+ * method.
+ * <p/>The dialog box opened is the same as the one in the ui created by
+ * {@link UiFlagAttributeNode#createUiControl(Composite, org.eclipse.ui.forms.IManagedForm)}
+ */
+public class FlagValueCellEditor extends EditableDialogCellEditor {
+
+ private UiFlagAttributeNode mUiFlagAttribute;
+
+ public FlagValueCellEditor(Composite parent) {
+ super(parent);
+ }
+
+ @Override
+ protected Object openDialogBox(Control cellEditorWindow) {
+ if (mUiFlagAttribute != null) {
+ String currentValue = (String)getValue();
+ return mUiFlagAttribute.showDialog(cellEditorWindow.getShell(), currentValue);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void doSetValue(Object value) {
+ if (value instanceof UiFlagAttributeNode) {
+ mUiFlagAttribute = (UiFlagAttributeNode)value;
+ super.doSetValue(mUiFlagAttribute.getCurrentValue());
+ return;
+ }
+
+ super.doSetValue(value);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ListValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ListValueCellEditor.java
new file mode 100644
index 000000000..0c780a8c7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ListValueCellEditor.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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;
+
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiListAttributeNode;
+
+import org.eclipse.jface.viewers.ComboBoxCellEditor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CCombo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/**
+ * ComboBoxCellEditor able to receive a {@link UiListAttributeNode} in the {@link #setValue(Object)}
+ * method, and returning a {@link String} in {@link #getValue()} instead of an {@link Integer}.
+ */
+public class ListValueCellEditor extends ComboBoxCellEditor {
+ private String[] mItems;
+ private CCombo mCombo;
+
+ public ListValueCellEditor(Composite parent) {
+ super(parent, new String[0], SWT.DROP_DOWN);
+ }
+
+ @Override
+ protected Control createControl(Composite parent) {
+ mCombo = (CCombo) super.createControl(parent);
+ return mCombo;
+ }
+
+ @Override
+ protected void doSetValue(Object value) {
+ if (value instanceof UiListAttributeNode) {
+ UiListAttributeNode uiListAttribute = (UiListAttributeNode)value;
+
+ // set the possible values in the combo
+ String[] items = uiListAttribute.getPossibleValues(null);
+ mItems = new String[items.length];
+ System.arraycopy(items, 0, mItems, 0, items.length);
+ setItems(mItems);
+
+ // now edit the current value of the attribute
+ String attrValue = uiListAttribute.getCurrentValue();
+ mCombo.setText(attrValue);
+
+ return;
+ }
+
+ // default behavior
+ super.doSetValue(value);
+ }
+
+ @Override
+ protected Object doGetValue() {
+ String comboText = mCombo.getText();
+ if (comboText == null) {
+ return ""; //$NON-NLS-1$
+ }
+ return comboText;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ResourceValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ResourceValueCellEditor.java
new file mode 100644
index 000000000..8efe294b1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/ResourceValueCellEditor.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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;
+
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiFlagAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiResourceAttributeNode;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/**
+ * DialogCellEditor able to receive a {@link UiFlagAttributeNode} in the {@link #setValue(Object)}
+ * method.
+ * <p/>The dialog box opened is the same as the one in the ui created by
+ * {@link UiFlagAttributeNode#createUiControl(Composite, org.eclipse.ui.forms.IManagedForm)}
+ */
+public class ResourceValueCellEditor extends EditableDialogCellEditor {
+
+ private UiResourceAttributeNode mUiResourceAttribute;
+
+ public ResourceValueCellEditor(Composite parent) {
+ super(parent);
+ }
+
+ @Override
+ protected Object openDialogBox(Control cellEditorWindow) {
+ if (mUiResourceAttribute != null) {
+ String currentValue = (String)getValue();
+ return mUiResourceAttribute.showDialog(cellEditorWindow.getShell(), currentValue);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void doSetValue(Object value) {
+ if (value instanceof UiResourceAttributeNode) {
+ mUiResourceAttribute = (UiResourceAttributeNode)value;
+ super.doSetValue(mUiResourceAttribute.getCurrentValue());
+ return;
+ }
+
+ super.doSetValue(value);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/SectionHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/SectionHelper.java
new file mode 100644
index 000000000..fdb5d8292
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/SectionHelper.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+
+import org.eclipse.jface.text.DefaultInformationControl;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.forms.SectionPart;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+import org.eclipse.ui.forms.widgets.TableWrapLayout;
+
+import java.lang.reflect.Method;
+
+/**
+ * Helper for the AndroidManifest form editor.
+ *
+ * Helps create a new SectionPart with sensible default parameters,
+ * create default layout or add typical widgets.
+ *
+ * IMPORTANT: This is NOT a generic class. It makes a lot of assumptions on the
+ * UI as used by the form editor for the AndroidManifest.
+ *
+ * TODO: Consider moving to a common package.
+ */
+public final class SectionHelper {
+
+ /**
+ * Utility class that derives from SectionPart, constructs the Section with
+ * sensible defaults (with a title and a description) and provide some shorthand
+ * methods for creating typically UI (label and text, form text.)
+ */
+ static public class ManifestSectionPart extends SectionPart {
+
+ /**
+ * Construct a SectionPart that uses a title bar and a description.
+ * It's up to the caller to call setText() and setDescription().
+ * <p/>
+ * The section style includes a description and a title bar by default.
+ *
+ * @param body The parent (e.g. FormPage body)
+ * @param toolkit Form Toolkit
+ */
+ public ManifestSectionPart(Composite body, FormToolkit toolkit) {
+ this(body, toolkit, 0, false);
+ }
+
+ /**
+ * Construct a SectionPart that uses a title bar and a description.
+ * It's up to the caller to call setText() and setDescription().
+ * <p/>
+ * The section style includes a description and a title bar by default.
+ * You can add extra styles, like Section.TWISTIE.
+ *
+ * @param body The parent (e.g. FormPage body).
+ * @param toolkit Form Toolkit.
+ * @param extra_style Extra styles (on top of description and title bar).
+ * @param use_description True if the Section.DESCRIPTION style should be added.
+ */
+ public ManifestSectionPart(Composite body, FormToolkit toolkit,
+ int extra_style, boolean use_description) {
+ super(body, toolkit, extra_style |
+ Section.TITLE_BAR |
+ (use_description ? Section.DESCRIPTION : 0));
+ }
+
+ // Create non-static methods of helpers just for convenience
+
+ /**
+ * Creates a new composite with a TableWrapLayout set with a given number of columns.
+ *
+ * If the parent composite is a Section, the new composite is set as a client.
+ *
+ * @param toolkit Form Toolkit
+ * @param numColumns Desired number of columns.
+ * @return The new composite.
+ */
+ public Composite createTableLayout(FormToolkit toolkit, int numColumns) {
+ return SectionHelper.createTableLayout(getSection(), toolkit, numColumns);
+ }
+
+ /**
+ * Creates a label widget.
+ * If the parent layout if a TableWrapLayout, maximize it to span over all columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param label The string for the label.
+ * @param tooltip An optional tooltip for the label and text. Can be null.
+ * @return The new created label
+ */
+ public Label createLabel(Composite parent, FormToolkit toolkit, String label,
+ String tooltip) {
+ return SectionHelper.createLabel(parent, toolkit, label, tooltip);
+ }
+
+ /**
+ * Creates two widgets: a label and a text field.
+ *
+ * This expects the parent composite to have a TableWrapLayout with 2 columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param label The string for the label.
+ * @param value The initial value of the text field. Can be null.
+ * @param tooltip An optional tooltip for the label and text. Can be null.
+ * @return The new created Text field (the label is not returned)
+ */
+ public Text createLabelAndText(Composite parent, FormToolkit toolkit, String label,
+ String value, String tooltip) {
+ return SectionHelper.createLabelAndText(parent, toolkit, label, value, tooltip);
+ }
+
+ /**
+ * Creates a FormText widget.
+ *
+ * This expects the parent composite to have a TableWrapLayout with 2 columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param isHtml True if the form text will contain HTML that must be interpreted as
+ * rich text (i.e. parse tags & expand URLs).
+ * @param label The string for the label.
+ * @param setupLayoutData indicates whether the created form text receives a TableWrapData
+ * through the setLayoutData method. In some case, creating it will make the table parent
+ * huge, which we don't want.
+ * @return The new created FormText.
+ */
+ public FormText createFormText(Composite parent, FormToolkit toolkit, boolean isHtml,
+ String label, boolean setupLayoutData) {
+ return SectionHelper.createFormText(parent, toolkit, isHtml, label, setupLayoutData);
+ }
+
+ /**
+ * Forces the section to recompute its layout and redraw.
+ * This is needed after the content of the section has been changed at runtime.
+ */
+ public void layoutChanged() {
+ Section section = getSection();
+
+ // Calls getSection().reflow(), which is the same that Section calls
+ // when the expandable state is changed and the height changes.
+ // Since this is protected, some reflection is needed to invoke it.
+ try {
+ Method reflow;
+ reflow = section.getClass().getDeclaredMethod("reflow", (Class<?>[])null);
+ reflow.setAccessible(true);
+ reflow.invoke(section);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Error when invoking Section.reflow");
+ }
+
+ section.layout(true /* changed */, true /* all */);
+ }
+ }
+
+ /**
+ * Creates a new composite with a TableWrapLayout set with a given number of columns.
+ *
+ * If the parent composite is a Section, the new composite is set as a client.
+ *
+ * @param composite The parent (e.g. a Section or SectionPart)
+ * @param toolkit Form Toolkit
+ * @param numColumns Desired number of columns.
+ * @return The new composite.
+ */
+ static public Composite createTableLayout(Composite composite, FormToolkit toolkit,
+ int numColumns) {
+ Composite table = toolkit.createComposite(composite);
+ TableWrapLayout layout = new TableWrapLayout();
+ layout.numColumns = numColumns;
+ table.setLayout(layout);
+ toolkit.paintBordersFor(table);
+ if (composite instanceof Section) {
+ ((Section) composite).setClient(table);
+ }
+ return table;
+ }
+
+ /**
+ * Creates a new composite with a GridLayout set with a given number of columns.
+ *
+ * If the parent composite is a Section, the new composite is set as a client.
+ *
+ * @param composite The parent (e.g. a Section or SectionPart)
+ * @param toolkit Form Toolkit
+ * @param numColumns Desired number of columns.
+ * @return The new composite.
+ */
+ static public Composite createGridLayout(Composite composite, FormToolkit toolkit,
+ int numColumns) {
+ Composite grid = toolkit.createComposite(composite);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = numColumns;
+ grid.setLayout(layout);
+ toolkit.paintBordersFor(grid);
+ if (composite instanceof Section) {
+ ((Section) composite).setClient(grid);
+ }
+ return grid;
+ }
+
+ /**
+ * Creates two widgets: a label and a text field.
+ *
+ * This expects the parent composite to have a TableWrapLayout with 2 columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param label_text The string for the label.
+ * @param value The initial value of the text field. Can be null.
+ * @param tooltip An optional tooltip for the label and text. Can be null.
+ * @return The new created Text field (the label is not returned)
+ */
+ static public Text createLabelAndText(Composite parent, FormToolkit toolkit, String label_text,
+ String value, String tooltip) {
+ Label label = toolkit.createLabel(parent, label_text);
+ label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ Text text = toolkit.createText(parent, value);
+ text.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
+
+ addControlTooltip(label, tooltip);
+ return text;
+ }
+
+ /**
+ * Creates a label widget.
+ * If the parent layout if a TableWrapLayout, maximize it to span over all columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param label_text The string for the label.
+ * @param tooltip An optional tooltip for the label and text. Can be null.
+ * @return The new created label
+ */
+ static public Label createLabel(Composite parent, FormToolkit toolkit, String label_text,
+ String tooltip) {
+ Label label = toolkit.createLabel(parent, label_text);
+
+ TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB);
+ if (parent.getLayout() instanceof TableWrapLayout) {
+ twd.colspan = ((TableWrapLayout) parent.getLayout()).numColumns;
+ }
+ label.setLayoutData(twd);
+
+ addControlTooltip(label, tooltip);
+ return label;
+ }
+
+ /**
+ * Associates a tooltip with a control.
+ *
+ * This mirrors the behavior from org.eclipse.pde.internal.ui.editor.text.PDETextHover
+ *
+ * @param control The control to which associate the tooltip.
+ * @param tooltip The tooltip string. Can use \n for multi-lines. Will not display if null.
+ */
+ static public void addControlTooltip(final Control control, String tooltip) {
+ if (control == null || tooltip == null || tooltip.length() == 0) {
+ return;
+ }
+
+ // Some kinds of controls already properly implement tooltip display.
+ if (control instanceof Button) {
+ control.setToolTipText(tooltip);
+ return;
+ }
+
+ control.setToolTipText(null);
+
+ final DefaultInformationControl ic = new DefaultInformationControl(control.getShell());
+ ic.setInformation(tooltip);
+ Point sz = ic.computeSizeHint();
+ ic.setSize(sz.x, sz.y);
+ ic.setVisible(false); // initially hidden
+
+ control.addMouseTrackListener(new MouseTrackListener() {
+ @Override
+ public void mouseEnter(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseExit(MouseEvent e) {
+ ic.setVisible(false);
+ }
+
+ @Override
+ public void mouseHover(MouseEvent e) {
+ ic.setLocation(control.toDisplay(10, 25)); // same offset as in PDETextHover
+ ic.setVisible(true);
+ }
+ });
+ control.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ ic.dispose();
+ }
+ });
+ }
+
+ /**
+ * Creates a FormText widget.
+ *
+ * This expects the parent composite to have a TableWrapLayout with 2 columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param isHtml True if the form text will contain HTML that must be interpreted as
+ * rich text (i.e. parse tags & expand URLs).
+ * @param label The string for the label.
+ * @param setupLayoutData indicates whether the created form text receives a TableWrapData
+ * through the setLayoutData method. In some case, creating it will make the table parent
+ * huge, which we don't want.
+ * @return The new created FormText.
+ */
+ static public FormText createFormText(Composite parent, FormToolkit toolkit,
+ boolean isHtml, String label, boolean setupLayoutData) {
+ FormText text = toolkit.createFormText(parent, true /* track focus */);
+ if (setupLayoutData) {
+ TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB);
+ twd.maxWidth = AndroidXmlEditor.TEXT_WIDTH_HINT;
+ if (parent.getLayout() instanceof TableWrapLayout) {
+ twd.colspan = ((TableWrapLayout) parent.getLayout()).numColumns;
+ }
+ text.setLayoutData(twd);
+ }
+ text.setWhitespaceNormalized(true);
+ if (isHtml && !label.startsWith("<form>")) { //$NON-NLS-1$
+ // This assertion is violated, for example by the Class attribute for an activity
+ //assert label.startsWith("<form>") : "HTML for FormText must be wrapped in <form>...</form>"; //$NON-NLS-1$
+ label = "<form>" + label + "</form>"; //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ text.setText(label, isHtml /* parseTags */, isHtml /* expandURLs */);
+ return text;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/TextValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/TextValueCellEditor.java
new file mode 100644
index 000000000..3750c3472
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/TextValueCellEditor.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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;
+
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+
+import org.eclipse.jface.viewers.TextCellEditor;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * TextCellEditor able to receive a {@link UiAttributeNode} in the {@link #setValue(Object)}
+ * method.
+ */
+public class TextValueCellEditor extends TextCellEditor {
+
+ public TextValueCellEditor(Composite parent) {
+ super(parent);
+ }
+
+ @Override
+ protected void doSetValue(Object value) {
+ if (value instanceof UiAttributeNode) {
+ super.doSetValue(((UiAttributeNode)value).getCurrentValue());
+ return;
+ }
+
+ super.doSetValue(value);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/UiElementPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/UiElementPart.java
new file mode 100644
index 000000000..db9fa069f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/UiElementPart.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+/**
+ * Generic page's section part that displays all attributes of a given {@link UiElementNode}.
+ * <p/>
+ * This part is designed to be displayed in a page that has a table column layout.
+ * It is linked to a specific {@link UiElementNode} and automatically displays all of its
+ * attributes, manages its dirty state and commits the attributes when necessary.
+ * <p/>
+ * No derivation is needed unless the UI or workflow needs to be extended.
+ */
+public class UiElementPart extends ManifestSectionPart {
+
+ /** A reference to the container editor */
+ private ManifestEditor mEditor;
+ /** The {@link UiElementNode} manipulated by this SectionPart. It can be null. */
+ private UiElementNode mUiElementNode;
+ /** Table that contains all the attributes */
+ private Composite mTable;
+
+ public UiElementPart(Composite body, FormToolkit toolkit, ManifestEditor editor,
+ UiElementNode uiElementNode, String sectionTitle, String sectionDescription,
+ int extra_style) {
+ super(body, toolkit, extra_style, sectionDescription != null);
+ mEditor = editor;
+ mUiElementNode = uiElementNode;
+ setupSection(sectionTitle, sectionDescription);
+
+ if (uiElementNode == null) {
+ // This is serious and should never happen. Instead of crashing, simply abort.
+ // There will be no UI, which will prevent further damage.
+ AdtPlugin.log(IStatus.ERROR, "Missing node to edit!"); //$NON-NLS-1$
+ return;
+ }
+ }
+
+ /**
+ * Returns the Editor associated with this part.
+ */
+ public ManifestEditor getEditor() {
+ return mEditor;
+ }
+
+ /**
+ * Returns the {@link UiElementNode} associated with this part.
+ */
+ public UiElementNode getUiElementNode() {
+ return mUiElementNode;
+ }
+
+ /**
+ * Changes the element node handled by this part.
+ *
+ * @param uiElementNode The new element node for the part.
+ */
+ public void setUiElementNode(UiElementNode uiElementNode) {
+ mUiElementNode = uiElementNode;
+ }
+
+ /**
+ * Initializes the form part.
+ * <p/>
+ * This is called by the owning managed form as soon as the part is added to the form,
+ * which happens right after the part is actually created.
+ */
+ @Override
+ public void initialize(IManagedForm form) {
+ super.initialize(form);
+ createFormControls(form);
+ }
+
+ /**
+ * Setup the section that contains this part.
+ * <p/>
+ * This is called by the constructor to set the section's title and description
+ * with parameters given in the constructor.
+ * <br/>
+ * Derived class override this if needed, however in most cases the default
+ * implementation should be enough.
+ *
+ * @param sectionTitle The section part's title
+ * @param sectionDescription The section part's description
+ */
+ protected void setupSection(String sectionTitle, String sectionDescription) {
+ Section section = getSection();
+ section.setText(sectionTitle);
+ section.setDescription(sectionDescription);
+ }
+
+ /**
+ * Create the controls to edit the attributes for the given ElementDescriptor.
+ * <p/>
+ * This MUST not be called by the constructor. Instead it must be called from
+ * <code>initialize</code> (i.e. right after the form part is added to the managed form.)
+ * <p/>
+ * Derived classes can override this if necessary.
+ *
+ * @param managedForm The owner managed form
+ */
+ protected void createFormControls(IManagedForm managedForm) {
+ setTable(createTableLayout(managedForm.getToolkit(), 2 /* numColumns */));
+
+ createUiAttributes(managedForm);
+ }
+
+ /**
+ * Sets the table where the attribute UI needs to be created.
+ */
+ protected void setTable(Composite table) {
+ mTable = table;
+ }
+
+ /**
+ * Returns the table where the attribute UI needs to be created.
+ */
+ protected Composite getTable() {
+ return mTable;
+ }
+
+ /**
+ * Add all the attribute UI widgets into the underlying table layout.
+ *
+ * @param managedForm The owner managed form
+ */
+ protected void createUiAttributes(IManagedForm managedForm) {
+ Composite table = getTable();
+ if (table == null || managedForm == null) {
+ return;
+ }
+
+ // Remove any old UI controls
+ for (Control c : table.getChildren()) {
+ c.dispose();
+ }
+
+ fillTable(table, managedForm);
+
+ // Tell the section that the layout has changed.
+ layoutChanged();
+ }
+
+ /**
+ * Actually fills the table.
+ * This is called by {@link #createUiAttributes(IManagedForm)} to populate the new
+ * table. The default implementation is to use
+ * {@link #insertUiAttributes(UiElementNode, Composite, IManagedForm)} to actually
+ * place the attributes of the default {@link UiElementNode} in the table.
+ * <p/>
+ * Derived classes can override this to add controls in the table before or after.
+ *
+ * @param table The table to fill. It must have 2 columns.
+ * @param managedForm The managed form for new controls.
+ */
+ protected void fillTable(Composite table, IManagedForm managedForm) {
+ int inserted = insertUiAttributes(mUiElementNode, table, managedForm);
+
+ if (inserted == 0) {
+ createLabel(table, managedForm.getToolkit(),
+ "No attributes to display, waiting for SDK to finish loading...",
+ null /* tooltip */ );
+ }
+ }
+
+ /**
+ * Insert the UI attributes of the given {@link UiElementNode} in the given table.
+ *
+ * @param uiNode The {@link UiElementNode} that contains the attributes to display.
+ * Must not be null.
+ * @param table The table to fill. It must have 2 columns.
+ * @param managedForm The managed form for new controls.
+ * @return The number of UI attributes inserted. It is >= 0.
+ */
+ protected int insertUiAttributes(UiElementNode uiNode, Composite table, IManagedForm managedForm) {
+ if (uiNode == null || table == null || managedForm == null) {
+ return 0;
+ }
+
+ // To iterate over all attributes, we use the {@link ElementDescriptor} instead
+ // of the {@link UiElementNode} because the attributes' order is guaranteed in the
+ // descriptor but not in the node itself.
+ AttributeDescriptor[] attr_desc_list = uiNode.getAttributeDescriptors();
+ for (AttributeDescriptor attr_desc : attr_desc_list) {
+ if (attr_desc instanceof XmlnsAttributeDescriptor) {
+ // Do not show hidden attributes
+ continue;
+ }
+
+ UiAttributeNode ui_attr = uiNode.findUiAttribute(attr_desc);
+ if (ui_attr != null) {
+ ui_attr.createUiControl(table, managedForm);
+ } else {
+ // The XML has an extra attribute which wasn't declared in
+ // AndroidManifestDescriptors. This is not a problem, we just ignore it.
+ AdtPlugin.log(IStatus.WARNING,
+ "Attribute %1$s not declared in node %2$s, ignored.", //$NON-NLS-1$
+ attr_desc.getXmlLocalName(),
+ uiNode.getDescriptor().getXmlName());
+ }
+ }
+ return attr_desc_list.length;
+ }
+
+ /**
+ * Tests whether the part is dirty i.e. its widgets have state that is
+ * newer than the data in the model.
+ * <p/>
+ * This is done by iterating over all attributes and updating the super's
+ * internal dirty flag. Stop once at least one attribute is dirty.
+ *
+ * @return <code>true</code> if the part is dirty, <code>false</code>
+ * otherwise.
+ */
+ @Override
+ public boolean isDirty() {
+ if (mUiElementNode != null && !super.isDirty()) {
+ for (UiAttributeNode ui_attr : mUiElementNode.getAllUiAttributes()) {
+ if (ui_attr.isDirty()) {
+ markDirty();
+ break;
+ }
+ }
+ }
+ return super.isDirty();
+ }
+
+ /**
+ * If part is displaying information loaded from a model, this method
+ * instructs it to commit the new (modified) data back into the model.
+ *
+ * @param onSave
+ * indicates if commit is called during 'save' operation or for
+ * some other reason (for example, if form is contained in a
+ * wizard or a multi-page editor and the user is about to leave
+ * the page).
+ */
+ @Override
+ public void commit(boolean onSave) {
+ if (mUiElementNode != null) {
+ mEditor.wrapEditXmlModel(new Runnable() {
+ @Override
+ public void run() {
+ for (UiAttributeNode ui_attr : mUiElementNode.getAllUiAttributes()) {
+ ui_attr.commit();
+ }
+ }
+ });
+ }
+
+ // We need to call super's commit after we synchronized the nodes to make sure we
+ // reset the dirty flag after all the side effects from committing have occurred.
+ super.commit(onSave);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/CopyCutAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/CopyCutAction.java
new file mode 100644
index 000000000..3fe98bb23
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/CopyCutAction.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.apache.xml.serialize.Method;
+import org.apache.xml.serialize.OutputFormat;
+import org.apache.xml.serialize.XMLSerializer;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.xml.core.internal.document.NodeContainer;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Provides Cut and Copy actions for the tree nodes.
+ */
+@SuppressWarnings({"restriction", "deprecation"})
+public class CopyCutAction extends Action {
+ private List<UiElementNode> mUiNodes;
+ private boolean mPerformCut;
+ private final AndroidXmlEditor mEditor;
+ private final Clipboard mClipboard;
+ private final ICommitXml mXmlCommit;
+
+ /**
+ * Creates a new Copy or Cut action.
+ *
+ * @param selected The UI node to cut or copy. It *must* have a non-null XML node.
+ * @param performCut True if the operation is cut, false if it is copy.
+ */
+ public CopyCutAction(AndroidXmlEditor editor, Clipboard clipboard, ICommitXml xmlCommit,
+ UiElementNode selected, boolean performCut) {
+ this(editor, clipboard, xmlCommit, toList(selected), performCut);
+ }
+
+ /**
+ * Creates a new Copy or Cut action.
+ *
+ * @param selected The UI nodes to cut or copy. They *must* have a non-null XML node.
+ * The list becomes owned by the {@link CopyCutAction}.
+ * @param performCut True if the operation is cut, false if it is copy.
+ */
+ public CopyCutAction(AndroidXmlEditor editor, Clipboard clipboard, ICommitXml xmlCommit,
+ List<UiElementNode> selected, boolean performCut) {
+ super(performCut ? "Cut" : "Copy");
+ mEditor = editor;
+ mClipboard = clipboard;
+ mXmlCommit = xmlCommit;
+
+ ISharedImages images = PlatformUI.getWorkbench().getSharedImages();
+ if (performCut) {
+ setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT));
+ setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT_HOVER));
+ setDisabledImageDescriptor(
+ images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT_DISABLED));
+ } else {
+ setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY));
+ setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY_HOVER));
+ setDisabledImageDescriptor(
+ images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY_DISABLED));
+ }
+
+ mUiNodes = selected;
+ mPerformCut = performCut;
+ }
+
+ /**
+ * Performs the cut or copy action.
+ * First an XML serializer is used to turn the existing XML node into a valid
+ * XML fragment, which is added as text to the clipboard.
+ */
+ @Override
+ public void run() {
+ super.run();
+ if (mUiNodes == null || mUiNodes.size() < 1) {
+ return;
+ }
+
+ // Commit the current pages first, to make sure the XML is in sync.
+ // Committing may change the XML structure.
+ if (mXmlCommit != null) {
+ mXmlCommit.commitPendingXmlChanges();
+ }
+
+ StringBuilder allText = new StringBuilder();
+ ArrayList<UiElementNode> nodesToCut = mPerformCut ? new ArrayList<UiElementNode>() : null;
+
+ for (UiElementNode uiNode : mUiNodes) {
+ try {
+ Node xml_node = uiNode.getXmlNode();
+ if (xml_node == null) {
+ return;
+ }
+
+ String data = getXmlTextFromEditor(xml_node);
+
+ // In the unlikely event that IStructuredDocument failed to extract the text
+ // directly from the editor, try to fall back on a direct XML serialization
+ // of the XML node. This uses the generic Node interface with no SSE tricks.
+ if (data == null) {
+ data = getXmlTextFromSerialization(xml_node);
+ }
+
+ if (data != null) {
+ allText.append(data);
+ if (mPerformCut) {
+ // only remove notes to cut if we actually got some XML text from them
+ nodesToCut.add(uiNode);
+ }
+ }
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "CopyCutAction failed for UI node %1$s", //$NON-NLS-1$
+ uiNode.getBreadcrumbTrailDescription(true));
+ }
+ } // for uiNode
+
+ if (allText != null && allText.length() > 0) {
+ mClipboard.setContents(
+ new Object[] { allText.toString() },
+ new Transfer[] { TextTransfer.getInstance() });
+ if (mPerformCut) {
+ for (UiElementNode uiNode : nodesToCut) {
+ uiNode.deleteXmlNode();
+ }
+ }
+ }
+ }
+
+ /** Get the data directly from the editor. */
+ private String getXmlTextFromEditor(Node xml_node) {
+ String data = null;
+ IStructuredModel model = mEditor.getModelForRead();
+ try {
+ IStructuredDocument sse_doc = mEditor.getStructuredDocument();
+ if (xml_node instanceof NodeContainer) {
+ // The easy way to get the source of an SSE XML node.
+ data = ((NodeContainer) xml_node).getSource();
+ } else if (xml_node instanceof IndexedRegion && sse_doc != null) {
+ // Try harder.
+ IndexedRegion region = (IndexedRegion) xml_node;
+ int start = region.getStartOffset();
+ int end = region.getEndOffset();
+
+ if (end > start) {
+ data = sse_doc.get(start, end - start);
+ }
+ }
+ } catch (BadLocationException e) {
+ // the region offset was invalid. ignore.
+ } finally {
+ model.releaseFromRead();
+ }
+ return data;
+ }
+
+ /**
+ * Direct XML serialization of the XML node.
+ * <p/>
+ * This uses the generic Node interface with no SSE tricks. It's however slower
+ * and doesn't respect formatting (since serialization is involved instead of reading
+ * the actual text buffer.)
+ */
+ private String getXmlTextFromSerialization(Node xml_node) throws IOException {
+ String data;
+ StringWriter sw = new StringWriter();
+ XMLSerializer serializer = new XMLSerializer(sw,
+ new OutputFormat(Method.XML,
+ OutputFormat.Defaults.Encoding /* utf-8 */,
+ true /* indent */));
+ // Serialize will throw an IOException if it fails.
+ serializer.serialize((Element) xml_node);
+ data = sw.toString();
+ return data;
+ }
+
+ /**
+ * Static helper class to wrap on node into a list for the constructors.
+ */
+ private static ArrayList<UiElementNode> toList(UiElementNode selected) {
+ ArrayList<UiElementNode> list = null;
+ if (selected != null) {
+ list = new ArrayList<UiElementNode>(1);
+ list.add(selected);
+ }
+ return list;
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/ICommitXml.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/ICommitXml.java
new file mode 100644
index 000000000..067d1459e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/ICommitXml.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+/**
+ * Interface for an object that can commit its changes to the underlying XML model
+ */
+public interface ICommitXml {
+
+ /** Commits pending data to the underlying XML model. */
+ public void commitPendingXmlChanges();
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/NewItemSelectionDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/NewItemSelectionDialog.java
new file mode 100644
index 000000000..385a53a5f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/NewItemSelectionDialog.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.dialogs.AbstractElementListSelectionDialog;
+import org.eclipse.ui.dialogs.ISelectionStatusValidator;
+import org.eclipse.ui.part.FileEditorInput;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+/**
+ * A selection dialog to select the type of the new element node to
+ * create, either in the application node or the selected sub node.
+ */
+public class NewItemSelectionDialog extends AbstractElementListSelectionDialog {
+
+ /** The UI node selected in the tree view before creating the new item selection dialog.
+ * Can be null -- which means new items must be created in the root_node. */
+ private UiElementNode mSelectedUiNode;
+ /** The root node chosen by the user, either root_node or the one passed
+ * to the constructor if not null */
+ private UiElementNode mChosenRootNode;
+ private UiElementNode mLocalRootNode;
+ /** The descriptor of the elements to be displayed as root in this tree view. All elements
+ * of the same type in the root will be displayed. Can be null. */
+ private ElementDescriptor[] mDescriptorFilters;
+ /** The key for the {@link #setLastUsedXmlName(Object[])}. It corresponds to the full
+ * workspace path of the currently edited file, if this can be computed. This is computed
+ * by {@link #getLastUsedXmlName(UiElementNode)}, called from the constructor. */
+ private String mLastUsedKey;
+ /** A static map of known XML Names used for a given file. The map has full workspace
+ * paths as key and XML names as values. */
+ private static final Map<String, String> sLastUsedXmlName = new HashMap<String, String>();
+ /** The potential XML Name to initially select in the selection dialog. This is computed
+ * in the constructor and set by {@link #setInitialSelection(UiElementNode)}. */
+ private String mInitialXmlName;
+
+ /**
+ * Creates the new item selection dialog.
+ *
+ * @param shell The parent shell for the list.
+ * @param labelProvider ILabelProvider for the list.
+ * @param descriptorFilters The element allows at the root of the tree. Can be null.
+ * @param ui_node The selected node, or null if none is selected.
+ * @param root_node The root of the Ui Tree, either the UiDocumentNode or a sub-node.
+ */
+ public NewItemSelectionDialog(Shell shell, ILabelProvider labelProvider,
+ ElementDescriptor[] descriptorFilters,
+ UiElementNode ui_node,
+ UiElementNode root_node) {
+ super(shell, labelProvider);
+ mDescriptorFilters = descriptorFilters;
+ mLocalRootNode = root_node;
+
+ // Only accept the UI node if it is not the UI root node and it can have children.
+ // If the node cannot have children, select its parent as a potential target.
+ if (ui_node != null && ui_node != mLocalRootNode) {
+ if (ui_node.getDescriptor().hasChildren()) {
+ mSelectedUiNode = ui_node;
+ } else {
+ UiElementNode parent = ui_node.getUiParent();
+ if (parent != null && parent != mLocalRootNode) {
+ mSelectedUiNode = parent;
+ }
+ }
+ }
+
+ setHelpAvailable(false);
+ setMultipleSelection(false);
+
+ setValidator(new ISelectionStatusValidator() {
+ @Override
+ public IStatus validate(Object[] selection) {
+ if (selection.length == 1 && selection[0] instanceof ViewElementDescriptor) {
+ return new Status(IStatus.OK, // severity
+ AdtPlugin.PLUGIN_ID, //plugin id
+ IStatus.OK, // code
+ ((ViewElementDescriptor) selection[0]).getFullClassName(), //msg
+ null); // exception
+ } else if (selection.length == 1 && selection[0] instanceof ElementDescriptor) {
+ return new Status(IStatus.OK, // severity
+ AdtPlugin.PLUGIN_ID, //plugin id
+ IStatus.OK, // code
+ "", //$NON-NLS-1$ // msg
+ null); // exception
+ } else {
+ return new Status(IStatus.ERROR, // severity
+ AdtPlugin.PLUGIN_ID, //plugin id
+ IStatus.ERROR, // code
+ "Invalid selection", // msg, translatable
+ null); // exception
+ }
+ }
+ });
+
+ // Determine the initial selection using a couple heuristics.
+
+ // First check if we can get the last used node type for this file.
+ // The heuristic is that generally one keeps adding the same kind of items to the
+ // same file, so reusing the last used item type makes most sense.
+ String xmlName = getLastUsedXmlName(root_node);
+ if (xmlName == null) {
+ // Another heuristic is to find the most used item and default to that.
+ xmlName = getMostUsedXmlName(root_node);
+ }
+ if (xmlName == null) {
+ // Finally the last heuristic is to see if there's an item with a name
+ // similar to the edited file name.
+ xmlName = getLeafFileName(root_node);
+ }
+ // Set the potential name. Selecting the right item is done later by setInitialSelection().
+ mInitialXmlName = xmlName;
+ }
+
+ /**
+ * Returns a potential XML name based on the file name.
+ * The item name is marked with an asterisk to identify it as a partial match.
+ */
+ private String getLeafFileName(UiElementNode ui_node) {
+ if (ui_node != null) {
+ AndroidXmlEditor editor = ui_node.getEditor();
+ if (editor != null) {
+ IEditorInput editorInput = editor.getEditorInput();
+ if (editorInput instanceof FileEditorInput) {
+ IFile f = ((FileEditorInput) editorInput).getFile();
+ if (f != null) {
+ String leafName = f.getFullPath().removeFileExtension().lastSegment();
+ return "*" + leafName; //$NON-NLS-1$
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Given a potential non-null root node, this method looks for the currently edited
+ * file path and uses it as a key to retrieve the last used item for this file by this
+ * selection dialog. Returns null if nothing can be found, otherwise returns the string
+ * name of the item.
+ */
+ private String getLastUsedXmlName(UiElementNode ui_node) {
+ if (ui_node != null) {
+ AndroidXmlEditor editor = ui_node.getEditor();
+ if (editor != null) {
+ IEditorInput editorInput = editor.getEditorInput();
+ if (editorInput instanceof FileEditorInput) {
+ IFile f = ((FileEditorInput) editorInput).getFile();
+ if (f != null) {
+ mLastUsedKey = f.getFullPath().toPortableString();
+
+ return sLastUsedXmlName.get(mLastUsedKey);
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the last used item for this selection dialog for this file.
+ * @param objects The currently selected items. Only the first one is used if it is an
+ * {@link ElementDescriptor}.
+ */
+ private void setLastUsedXmlName(Object[] objects) {
+ if (mLastUsedKey != null &&
+ objects != null &&
+ objects.length > 0 &&
+ objects[0] instanceof ElementDescriptor) {
+ ElementDescriptor desc = (ElementDescriptor) objects[0];
+ sLastUsedXmlName.put(mLastUsedKey, desc.getXmlName());
+ }
+ }
+
+ /**
+ * Returns the most used sub-element name, if any, or null.
+ */
+ private String getMostUsedXmlName(UiElementNode ui_node) {
+ if (ui_node != null) {
+ TreeMap<String, Integer> counts = new TreeMap<String, Integer>();
+ int max = -1;
+
+ for (UiElementNode child : ui_node.getUiChildren()) {
+ String name = child.getDescriptor().getXmlName();
+ Integer i = counts.get(name);
+ int count = i == null ? 1 : i.intValue() + 1;
+ counts.put(name, count);
+ max = Math.max(max, count);
+ }
+
+ if (max > 0) {
+ // Find first key with this max and return it
+ for (Entry<String, Integer> entry : counts.entrySet()) {
+ if (entry.getValue().intValue() == max) {
+ return entry.getKey();
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return The root node selected by the user, either root node or the
+ * one passed to the constructor if not null.
+ */
+ public UiElementNode getChosenRootNode() {
+ return mChosenRootNode;
+ }
+
+ /**
+ * Internal helper to compute the result. Returns the selection from
+ * the list view, if any.
+ */
+ @Override
+ protected void computeResult() {
+ setResult(Arrays.asList(getSelectedElements()));
+ setLastUsedXmlName(getSelectedElements());
+ }
+
+ /**
+ * Creates the dialog area.
+ *
+ * First add a radio area, which may be either 2 radio controls or
+ * just a message area if there's only one choice (the app root node).
+ *
+ * Then uses the default from the AbstractElementListSelectionDialog
+ * which is to add both a filter text and a filtered list. Adding both
+ * is necessary (since the base class accesses both internal directly
+ * fields without checking for null pointers.)
+ *
+ * Finally sets the initial selection list.
+ */
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite contents = (Composite) super.createDialogArea(parent);
+
+ createRadioControl(contents);
+ createFilterText(contents);
+ createFilteredList(contents);
+
+ // We don't want the builtin message area label (we use a radio control
+ // instead), but if we don't create it, Bad Stuff happens on
+ // Eclipse 3.8 and later (see issue 32527).
+ Label label = createMessageArea(contents);
+ if (label != null) {
+ GridData data = (GridData) label.getLayoutData();
+ data.exclude = true;
+ }
+
+ // Initialize the list state.
+ // This must be done after the filtered list as been created.
+ chooseNode(mChosenRootNode);
+
+ // Set the initial selection
+ setInitialSelection(mChosenRootNode);
+ return contents;
+ }
+
+ /**
+ * Tries to set the initial selection based on the {@link #mInitialXmlName} computed
+ * in the constructor. The selection is only set if there's an element descriptor
+ * that matches the same exact XML name. When {@link #mInitialXmlName} starts with an
+ * asterisk, it means to do a partial case-insensitive match on the start of the
+ * strings.
+ */
+ private void setInitialSelection(UiElementNode rootNode) {
+ ElementDescriptor initialElement = null;
+
+ if (mInitialXmlName != null && mInitialXmlName.length() > 0) {
+ String name = mInitialXmlName;
+ boolean partial = name.startsWith("*"); //$NON-NLS-1$
+ if (partial) {
+ name = name.substring(1).toLowerCase(Locale.US);
+ }
+
+ for (ElementDescriptor desc : getAllowedDescriptors(rootNode)) {
+ if (!partial && desc.getXmlName().equals(name)) {
+ initialElement = desc;
+ break;
+ } else if (partial) {
+ String name2 = desc.getXmlLocalName().toLowerCase(Locale.US);
+ if (name.startsWith(name2) || name2.startsWith(name)) {
+ initialElement = desc;
+ break;
+ }
+ }
+ }
+ }
+
+ setSelection(initialElement == null ? null : new ElementDescriptor[] { initialElement });
+ }
+
+ /**
+ * Creates the message text widget and sets layout data.
+ * @param content the parent composite of the message area.
+ */
+ private Composite createRadioControl(Composite content) {
+
+ if (mSelectedUiNode != null) {
+ Button radio1 = new Button(content, SWT.RADIO);
+ radio1.setText(String.format("Create a new element at the top level, in %1$s.",
+ mLocalRootNode.getShortDescription()));
+
+ Button radio2 = new Button(content, SWT.RADIO);
+ radio2.setText(String.format("Create a new element in the selected element, %1$s.",
+ mSelectedUiNode.getBreadcrumbTrailDescription(false /* include_root */)));
+
+ // Set the initial selection before adding the listeners
+ // (they can't be run till the filtered list has been created)
+ radio1.setSelection(false);
+ radio2.setSelection(true);
+ mChosenRootNode = mSelectedUiNode;
+
+ radio1.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ chooseNode(mLocalRootNode);
+ }
+ });
+
+ radio2.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ chooseNode(mSelectedUiNode);
+ }
+ });
+ } else {
+ setMessage(String.format("Create a new element at the top level, in %1$s.",
+ mLocalRootNode.getShortDescription()));
+ createMessageArea(content);
+
+ mChosenRootNode = mLocalRootNode;
+ }
+
+ return content;
+ }
+
+ /**
+ * Internal helper to remember the root node choosen by the user.
+ * It also sets the list view to the adequate list of children that can
+ * be added to the chosen root node.
+ *
+ * If the chosen root node is mLocalRootNode and a descriptor filter was specified
+ * when creating the master-detail part, we use this as the set of nodes that
+ * can be created on the root node.
+ *
+ * @param ui_node The chosen root node, either mLocalRootNode or
+ * mSelectedUiNode.
+ */
+ private void chooseNode(UiElementNode ui_node) {
+ mChosenRootNode = ui_node;
+ setListElements(getAllowedDescriptors(ui_node));
+ }
+
+ /**
+ * Returns the list of {@link ElementDescriptor}s that can be added to the given
+ * UI node.
+ *
+ * @param ui_node The UI node to which element should be added. Cannot be null.
+ * @return A non-null array of {@link ElementDescriptor}. The array might be empty.
+ */
+ private ElementDescriptor[] getAllowedDescriptors(UiElementNode ui_node) {
+ if (ui_node == mLocalRootNode &&
+ mDescriptorFilters != null &&
+ mDescriptorFilters.length != 0) {
+ return mDescriptorFilters;
+ } else {
+ return ui_node.getDescriptor().getChildren();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/PasteAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/PasteAction.java
new file mode 100644
index 000000000..6674ba9ca
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/PasteAction.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
+import org.eclipse.wst.xml.core.internal.document.NodeContainer;
+import org.w3c.dom.Node;
+
+
+/**
+ * Provides Paste operation for the tree nodes
+ */
+@SuppressWarnings("restriction")
+public class PasteAction extends Action {
+ private UiElementNode mUiNode;
+ private final AndroidXmlEditor mEditor;
+ private final Clipboard mClipboard;
+
+ public PasteAction(AndroidXmlEditor editor, Clipboard clipboard, UiElementNode ui_node) {
+ super("Paste");
+ mEditor = editor;
+ mClipboard = clipboard;
+
+ ISharedImages images = PlatformUI.getWorkbench().getSharedImages();
+ setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_PASTE));
+ setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_PASTE));
+ setDisabledImageDescriptor(
+ images.getImageDescriptor(ISharedImages.IMG_TOOL_PASTE_DISABLED));
+
+ mUiNode = ui_node;
+ }
+
+ /**
+ * Performs the paste operation.
+ */
+ @Override
+ public void run() {
+ super.run();
+
+ final String data = (String) mClipboard.getContents(TextTransfer.getInstance());
+ if (data != null) {
+ mEditor.wrapEditXmlModel(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ IStructuredDocument sse_doc = mEditor.getStructuredDocument();
+ if (sse_doc != null) {
+ if (mUiNode.getDescriptor().hasChildren()) {
+ // This UI Node can have children. The new XML is
+ // inserted as the first child.
+
+ if (mUiNode.getUiChildren().size() > 0) {
+ // There's already at least one child,
+ // so insert right before it.
+ Node xml_node = mUiNode.getUiChildren().get(0).getXmlNode();
+
+ if (xml_node instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) xml_node;
+ sse_doc.replace(region.getStartOffset(), 0, data);
+ return; // we're done, no need to try the other cases
+ }
+ }
+
+ // If there's no first XML node child. Create one by
+ // inserting at the end of the *start* tag.
+ Node xml_node = mUiNode.getXmlNode();
+ if (xml_node instanceof NodeContainer) {
+ NodeContainer container = (NodeContainer) xml_node;
+ IStructuredDocumentRegion start_tag =
+ container.getStartStructuredDocumentRegion();
+ if (start_tag != null) {
+ sse_doc.replace(start_tag.getEndOffset(), 0, data);
+ return; // we're done, no need to try the other case
+ }
+ }
+ }
+
+ // This UI Node doesn't accept children. The new XML is inserted as the
+ // next sibling. This also serves as a fallback if all the previous
+ // attempts failed. However, this is not possible if the current node
+ // has for parent a document -- an XML document can only have one root,
+ // with no siblings.
+ if (!(mUiNode.getUiParent() instanceof UiDocumentNode)) {
+ Node xml_node = mUiNode.getXmlNode();
+ if (xml_node instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) xml_node;
+ sse_doc.replace(region.getEndOffset(), 0, data);
+ }
+ }
+ }
+
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e,
+ "ParseAction failed for UI Node %2$s, content '%1$s'", //$NON-NLS-1$
+ mUiNode.getBreadcrumbTrailDescription(true), data);
+ }
+ }
+ });
+ }
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiActions.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiActions.java
new file mode 100644
index 000000000..92ccf2e7d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiActions.java
@@ -0,0 +1,598 @@
+/*
+ * 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;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiElementDetail.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiElementDetail.java
new file mode 100644
index 000000000..2aa56a826
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiElementDetail.java
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+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.SeparatorAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.ui.forms.IDetailsPage;
+import org.eclipse.ui.forms.IFormPart;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.events.ExpansionEvent;
+import org.eclipse.ui.forms.events.IExpansionListener;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+import org.eclipse.ui.forms.widgets.SharedScrolledComposite;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+import org.eclipse.ui.forms.widgets.TableWrapLayout;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+/**
+ * Details page for the {@link UiElementNode} nodes in the tree view.
+ * <p/>
+ * See IDetailsBase for more details.
+ */
+class UiElementDetail implements IDetailsPage {
+
+ /** The master-detail part, composed of a main tree and an auxiliary detail part */
+ private ManifestSectionPart mMasterPart;
+
+ private Section mMasterSection;
+ private UiElementNode mCurrentUiElementNode;
+ private Composite mCurrentTable;
+ private boolean mIsDirty;
+
+ private IManagedForm mManagedForm;
+
+ private final UiTreeBlock mTree;
+
+ public UiElementDetail(UiTreeBlock tree) {
+ mTree = tree;
+ mMasterPart = mTree.getMasterPart();
+ mManagedForm = mMasterPart.getManagedForm();
+ }
+
+ /* (non-java doc)
+ * Initializes the part.
+ */
+ @Override
+ public void initialize(IManagedForm form) {
+ mManagedForm = form;
+ }
+
+ /* (non-java doc)
+ * Creates the contents of the page in the provided parent.
+ */
+ @Override
+ public void createContents(Composite parent) {
+ mMasterSection = createMasterSection(parent);
+ }
+
+ /* (non-java doc)
+ * Called when the provided part has changed selection state.
+ * <p/>
+ * Only reply when our master part originates the selection.
+ */
+ @Override
+ public void selectionChanged(IFormPart part, ISelection selection) {
+ if (part == mMasterPart &&
+ !selection.isEmpty() &&
+ selection instanceof ITreeSelection) {
+ ITreeSelection tree_selection = (ITreeSelection) selection;
+
+ Object first = tree_selection.getFirstElement();
+ if (first instanceof UiElementNode) {
+ UiElementNode ui_node = (UiElementNode) first;
+ createUiAttributeControls(mManagedForm, ui_node);
+ }
+ }
+ }
+
+ /* (non-java doc)
+ * Instructs it to commit the new (modified) data back into the model.
+ */
+ @Override
+ public void commit(boolean onSave) {
+
+ mTree.getEditor().wrapEditXmlModel(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (mCurrentUiElementNode != null) {
+ mCurrentUiElementNode.commit();
+ }
+
+ // Finally reset the dirty flag if everything was saved properly
+ mIsDirty = false;
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Detail node failed to commit XML attribute!"); //$NON-NLS-1$
+ }
+ }
+ });
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+
+ /* (non-java doc)
+ * Returns true if the part has been modified with respect to the data
+ * loaded from the model.
+ */
+ @Override
+ public boolean isDirty() {
+ if (mCurrentUiElementNode != null && mCurrentUiElementNode.isDirty()) {
+ markDirty();
+ }
+ return mIsDirty;
+ }
+
+ @Override
+ public boolean isStale() {
+ // pass
+ return false;
+ }
+
+ /**
+ * Called by the master part when the tree is refreshed after the framework resources
+ * have been reloaded.
+ */
+ @Override
+ public void refresh() {
+ if (mCurrentTable != null) {
+ mCurrentTable.dispose();
+ mCurrentTable = null;
+ }
+ mCurrentUiElementNode = null;
+ mMasterSection.getParent().pack(true /* changed */);
+ }
+
+ @Override
+ public void setFocus() {
+ // pass
+ }
+
+ @Override
+ public boolean setFormInput(Object input) {
+ // pass
+ return false;
+ }
+
+ /**
+ * Creates a TableWrapLayout in the DetailsPage, which in turns contains a Section.
+ *
+ * All the UI should be created in a layout which parent is the mSection itself.
+ * The hierarchy is:
+ * <pre>
+ * DetailPage
+ * + TableWrapLayout
+ * + Section (with title/description && fill_grab horizontal)
+ * + TableWrapLayout [*]
+ * + Labels/Forms/etc... [*]
+ * </pre>
+ * Both items marked with [*] are created by the derived classes to fit their needs.
+ *
+ * @param parent Parent of the mSection (from createContents)
+ * @return The new Section
+ */
+ private Section createMasterSection(Composite parent) {
+ TableWrapLayout layout = new TableWrapLayout();
+ layout.topMargin = 0;
+ parent.setLayout(layout);
+
+ FormToolkit toolkit = mManagedForm.getToolkit();
+ Section section = toolkit.createSection(parent, Section.TITLE_BAR);
+ section.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.TOP));
+ return section;
+ }
+
+ /**
+ * Create the ui attribute controls to edit the attributes for the given
+ * ElementDescriptor.
+ * <p/>
+ * This is called by the constructor.
+ * Derived classes can override this if necessary.
+ *
+ * @param managedForm The managed form
+ */
+ private void createUiAttributeControls(
+ final IManagedForm managedForm,
+ final UiElementNode ui_node) {
+
+ final ElementDescriptor elem_desc = ui_node.getDescriptor();
+ mMasterSection.setText(String.format("Attributes for %1$s", ui_node.getShortDescription()));
+
+ if (mCurrentUiElementNode != ui_node) {
+ // Before changing the table, commit all dirty state.
+ if (mIsDirty) {
+ commit(false);
+ }
+ if (mCurrentTable != null) {
+ mCurrentTable.dispose();
+ mCurrentTable = null;
+ }
+
+ // To iterate over all attributes, we use the {@link ElementDescriptor} instead
+ // of the {@link UiElementNode} because the attributes order is guaranteed in the
+ // descriptor but not in the node itself.
+ AttributeDescriptor[] attr_desc_list = ui_node.getAttributeDescriptors();
+
+ // If the attribute list contains at least one SeparatorAttributeDescriptor,
+ // sub-sections will be used. This needs to be known early as it influences the
+ // creation of the master table.
+ boolean useSubsections = false;
+ for (AttributeDescriptor attr_desc : attr_desc_list) {
+ if (attr_desc instanceof SeparatorAttributeDescriptor) {
+ // Sub-sections will be used. The default sections should no longer be
+ useSubsections = true;
+ break;
+ }
+ }
+
+ FormToolkit toolkit = managedForm.getToolkit();
+ Composite masterTable = SectionHelper.createTableLayout(mMasterSection,
+ toolkit, useSubsections ? 1 : 2 /* numColumns */);
+ mCurrentTable = masterTable;
+
+ mCurrentUiElementNode = ui_node;
+
+ if (elem_desc.getTooltip() != null) {
+ String tooltip;
+ if (Sdk.getCurrent() != null &&
+ Sdk.getCurrent().getDocumentationBaseUrl() != null) {
+ tooltip = DescriptorsUtils.formatFormText(elem_desc.getTooltip(),
+ elem_desc,
+ Sdk.getCurrent().getDocumentationBaseUrl());
+ } else {
+ tooltip = elem_desc.getTooltip();
+ }
+
+ try {
+ FormText text = SectionHelper.createFormText(masterTable, toolkit,
+ true /* isHtml */, tooltip, true /* setupLayoutData */);
+ text.addHyperlinkListener(mTree.getEditor().createHyperlinkListener());
+ Image icon = elem_desc.getCustomizedIcon();
+ if (icon != null) {
+ text.setImage(DescriptorsUtils.IMAGE_KEY, icon);
+ }
+ } catch(Exception e) {
+ // The FormText parser is really really basic and will fail as soon as the
+ // HTML javadoc is ever so slightly malformatted.
+ AdtPlugin.log(e,
+ "Malformed javadoc, rejected by FormText for node %1$s: '%2$s'", //$NON-NLS-1$
+ ui_node.getDescriptor().getXmlName(),
+ tooltip);
+
+ // Fallback to a pure text tooltip, no fancy HTML
+ tooltip = DescriptorsUtils.formatTooltip(elem_desc.getTooltip());
+ SectionHelper.createLabel(masterTable, toolkit, tooltip, tooltip);
+ }
+ }
+
+ Composite table = useSubsections ? null : masterTable;
+
+ for (AttributeDescriptor attr_desc : attr_desc_list) {
+ if (attr_desc instanceof XmlnsAttributeDescriptor) {
+ // Do not show hidden attributes
+ continue;
+ } else if (table == null || attr_desc instanceof SeparatorAttributeDescriptor) {
+ String title = null;
+ if (attr_desc instanceof SeparatorAttributeDescriptor) {
+ // xmlName is actually the label of the separator
+ title = attr_desc.getXmlLocalName();
+ } else {
+ title = String.format("Attributes from %1$s", elem_desc.getUiName());
+ }
+
+ table = createSubSectionTable(toolkit, masterTable, title);
+ if (attr_desc instanceof SeparatorAttributeDescriptor) {
+ continue;
+ }
+ }
+
+ UiAttributeNode ui_attr = ui_node.findUiAttribute(attr_desc);
+
+ if (ui_attr != null) {
+ ui_attr.createUiControl(table, managedForm);
+
+ if (ui_attr.getCurrentValue() != null &&
+ ui_attr.getCurrentValue().length() > 0) {
+ ((Section) table.getParent()).setExpanded(true);
+ }
+ } else {
+ // The XML has an extra unknown attribute.
+ // This is not expected to happen so it is ignored.
+ AdtPlugin.log(IStatus.INFO,
+ "Attribute %1$s not declared in node %2$s, ignored.", //$NON-NLS-1$
+ attr_desc.getXmlLocalName(),
+ ui_node.getDescriptor().getXmlName());
+ }
+ }
+
+ // Create a sub-section for the unknown attributes.
+ // It is initially hidden till there are some attributes to show here.
+ final Composite unknownTable = createSubSectionTable(toolkit, masterTable,
+ "Unknown XML Attributes");
+ unknownTable.getParent().setVisible(false); // set section to not visible
+ final HashSet<UiAttributeNode> reference = new HashSet<UiAttributeNode>();
+
+ final IUiUpdateListener updateListener = new IUiUpdateListener() {
+ @Override
+ public void uiElementNodeUpdated(UiElementNode uiNode, UiUpdateState state) {
+ if (state == UiUpdateState.ATTR_UPDATED) {
+ updateUnknownAttributesSection(uiNode, unknownTable, managedForm,
+ reference);
+ }
+ }
+ };
+ ui_node.addUpdateListener(updateListener);
+
+ // remove the listener when the UI is disposed
+ unknownTable.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ ui_node.removeUpdateListener(updateListener);
+ }
+ });
+
+ updateUnknownAttributesSection(ui_node, unknownTable, managedForm, reference);
+
+ mMasterSection.getParent().pack(true /* changed */);
+ }
+ }
+
+ /**
+ * Create a sub Section and its embedding wrapper table with 2 columns.
+ * @return The table, child of a new section.
+ */
+ private Composite createSubSectionTable(FormToolkit toolkit,
+ Composite masterTable, String title) {
+
+ // The Section composite seems to ignore colspan when assigned a TableWrapData so
+ // if the parent is a table with more than one column an extra table with one column
+ // is inserted to respect colspan.
+ int parentNumCol = ((TableWrapLayout) masterTable.getLayout()).numColumns;
+ if (parentNumCol > 1) {
+ masterTable = SectionHelper.createTableLayout(masterTable, toolkit, 1);
+ TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB);
+ twd.maxWidth = AndroidXmlEditor.TEXT_WIDTH_HINT;
+ twd.colspan = parentNumCol;
+ masterTable.setLayoutData(twd);
+ }
+
+ Composite table;
+ Section section = toolkit.createSection(masterTable,
+ Section.TITLE_BAR | Section.TWISTIE);
+
+ // Add an expansion listener that will trigger a reflow on the parent
+ // ScrolledPageBook (which is actually a SharedScrolledComposite). This will
+ // recompute the correct size and adjust the scrollbar as needed.
+ section.addExpansionListener(new IExpansionListener() {
+ @Override
+ public void expansionStateChanged(ExpansionEvent e) {
+ reflowMasterSection();
+ }
+
+ @Override
+ public void expansionStateChanging(ExpansionEvent e) {
+ // pass
+ }
+ });
+
+ section.setText(title);
+ section.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB,
+ TableWrapData.TOP));
+ table = SectionHelper.createTableLayout(section, toolkit, 2 /* numColumns */);
+ return table;
+ }
+
+ /**
+ * Reflow the parent ScrolledPageBook (which is actually a SharedScrolledComposite).
+ * This will recompute the correct size and adjust the scrollbar as needed.
+ */
+ private void reflowMasterSection() {
+ for(Composite c = mMasterSection; c != null; c = c.getParent()) {
+ if (c instanceof SharedScrolledComposite) {
+ ((SharedScrolledComposite) c).reflow(true /* flushCache */);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Updates the unknown attributes section for the UI Node.
+ */
+ private void updateUnknownAttributesSection(UiElementNode ui_node,
+ final Composite unknownTable, final IManagedForm managedForm,
+ HashSet<UiAttributeNode> reference) {
+ Collection<UiAttributeNode> ui_attrs = ui_node.getUnknownUiAttributes();
+ Section section = ((Section) unknownTable.getParent());
+ boolean needs_reflow = false;
+
+ // The table was created hidden, show it if there are unknown attributes now
+ if (ui_attrs.size() > 0 && !section.isVisible()) {
+ section.setVisible(true);
+ needs_reflow = true;
+ }
+
+ // Compare the new attribute set with the old "reference" one
+ boolean has_differences = ui_attrs.size() != reference.size();
+ if (!has_differences) {
+ for (UiAttributeNode ui_attr : ui_attrs) {
+ if (!reference.contains(ui_attr)) {
+ has_differences = true;
+ break;
+ }
+ }
+ }
+
+ if (has_differences) {
+ needs_reflow = true;
+ reference.clear();
+
+ // Remove all children of the table
+ for (Control c : unknownTable.getChildren()) {
+ c.dispose();
+ }
+
+ // Recreate all attributes UI
+ for (UiAttributeNode ui_attr : ui_attrs) {
+ reference.add(ui_attr);
+ ui_attr.createUiControl(unknownTable, managedForm);
+
+ if (ui_attr.getCurrentValue() != null && ui_attr.getCurrentValue().length() > 0) {
+ section.setExpanded(true);
+ }
+ }
+ }
+
+ if (needs_reflow) {
+ reflowMasterSection();
+ }
+ }
+
+ /**
+ * Marks the part dirty. Called as a result of user interaction with the widgets in the
+ * section.
+ */
+ private void markDirty() {
+ if (!mIsDirty) {
+ mIsDirty = true;
+ mManagedForm.dirtyStateChanged();
+ }
+ }
+}
+
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeContentProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeContentProvider.java
new file mode 100644
index 000000000..14049cf86
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeContentProvider.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+import java.util.ArrayList;
+
+/**
+ * UiModelTreeContentProvider is a trivial implementation of {@link ITreeContentProvider}
+ * where elements are expected to be instances of {@link UiElementNode}.
+ */
+class UiModelTreeContentProvider implements ITreeContentProvider {
+
+ /** The descriptor of the elements to be displayed as root in this tree view. All elements
+ * of the same type in the root will be displayed. */
+ private ElementDescriptor[] mDescriptorFilters;
+ /** The uiRootNode of the model. */
+ private final UiElementNode mUiRootNode;
+
+ public UiModelTreeContentProvider(UiElementNode uiRootNode,
+ ElementDescriptor[] descriptorFilters) {
+ mUiRootNode = uiRootNode;
+ mDescriptorFilters = descriptorFilters;
+ }
+
+ /* (non-java doc)
+ * Returns all the UI node children of the given element or null if not the right kind
+ * of object. */
+ @Override
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) parentElement;
+ return node.getUiChildren().toArray();
+ }
+ return null;
+ }
+
+ /* (non-java doc)
+ * Returns the parent of a given UI node or null if it's a root node or it's not the
+ * right kind of node. */
+ @Override
+ public Object getParent(Object element) {
+ if (element instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) element;
+ return node.getUiParent();
+ }
+ return null;
+ }
+
+ /* (non-java doc)
+ * Returns true if the UI node has any UI children nodes. */
+ @Override
+ public boolean hasChildren(Object element) {
+ if (element instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) element;
+ return node.getUiChildren().size() > 0;
+ }
+ return false;
+ }
+
+ /* (non-java doc)
+ * Get root elements for the tree. These are all the UI nodes that
+ * match the filter descriptor in the current root node.
+ * <p/>
+ * Although not documented, it seems this method should not return null.
+ * At worse, it should return new Object[0].
+ * <p/>
+ * inputElement is not currently used. The root node and the filter are given
+ * by the enclosing class.
+ */
+ @Override
+ public Object[] getElements(Object inputElement) {
+ ArrayList<UiElementNode> roots = new ArrayList<UiElementNode>();
+ if (mUiRootNode != null) {
+ for (UiElementNode ui_node : mUiRootNode.getUiChildren()) {
+ if (mDescriptorFilters == null || mDescriptorFilters.length == 0) {
+ roots.add(ui_node);
+ } else {
+ for (ElementDescriptor filter : mDescriptorFilters) {
+ if (ui_node.getDescriptor() == filter) {
+ roots.add(ui_node);
+ }
+ }
+ }
+ }
+ }
+
+ return roots.toArray();
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeLabelProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeLabelProvider.java
new file mode 100644
index 000000000..337319761
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiModelTreeLabelProvider.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * UiModelTreeLabelProvider is a trivial implementation of {@link ILabelProvider}
+ * where elements are expected to derive from {@link UiElementNode} or
+ * from {@link ElementDescriptor}.
+ *
+ * It is used by both the master tree viewer and by the list in the Add... selection dialog.
+ */
+public class UiModelTreeLabelProvider implements ILabelProvider {
+
+ public UiModelTreeLabelProvider() {
+ }
+
+ /**
+ * Returns the element's logo with a fallback on the android logo.
+ */
+ @Override
+ public Image getImage(Object element) {
+ ElementDescriptor desc = null;
+ UiElementNode node = null;
+
+ if (element instanceof ElementDescriptor) {
+ desc = (ElementDescriptor) element;
+ } else if (element instanceof UiElementNode) {
+ node = (UiElementNode) element;
+ desc = node.getDescriptor();
+ }
+
+ if (desc != null) {
+ Image img = desc.getCustomizedIcon();
+ if (img != null) {
+ if (node != null && node.hasError()) {
+ return IconFactory.getInstance().addErrorIcon(img);
+ } else {
+ return img;
+ }
+ }
+ }
+
+ return AdtPlugin.getAndroidLogo();
+ }
+
+ /**
+ * Uses UiElementNode.shortDescription for the label for this tree item.
+ */
+ @Override
+ public String getText(Object element) {
+ if (element instanceof ElementDescriptor) {
+ ElementDescriptor desc = (ElementDescriptor) element;
+ return desc.getUiName();
+ } else if (element instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) element;
+ return node.getShortDescription();
+ }
+ return element.toString();
+ }
+
+ @Override
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ @Override
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+}
+
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiTreeBlock.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiTreeBlock.java
new file mode 100644
index 000000000..d11b8a4c6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiTreeBlock.java
@@ -0,0 +1,946 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
+import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IMenuListener;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerComparator;
+import org.eclipse.jface.viewers.ViewerFilter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.ui.forms.DetailsPart;
+import org.eclipse.ui.forms.IDetailsPage;
+import org.eclipse.ui.forms.IDetailsPageProvider;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.MasterDetailsBlock;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+/**
+ * {@link UiTreeBlock} is a {@link MasterDetailsBlock} which displays a tree view for
+ * a specific set of {@link UiElementNode}.
+ * <p/>
+ * For a given UI element node, the tree view displays all first-level children that
+ * match a given type (given by an {@link ElementDescriptor}. All children from these
+ * nodes are also displayed.
+ * <p/>
+ * In the middle next to the tree are some controls to add or delete tree nodes.
+ * On the left is a details part that displays all the visible UI attributes for a given
+ * selected UI element node.
+ */
+public final class UiTreeBlock extends MasterDetailsBlock implements ICommitXml {
+
+ /** Height hint for the tree view. Helps the grid layout resize properly on smaller screens. */
+ private static final int TREE_HEIGHT_HINT = 50;
+
+ /** Container editor */
+ AndroidXmlEditor mEditor;
+ /** The root {@link UiElementNode} which contains all the elements that are to be
+ * manipulated by this tree view. In general this is the manifest UI node. */
+ private UiElementNode mUiRootNode;
+ /** The descriptor of the elements to be displayed as root in this tree view. All elements
+ * of the same type in the root will be displayed. Can be null or empty to mean everything
+ * can be displayed. */
+ private ElementDescriptor[] mDescriptorFilters;
+ /** The title for the master-detail part (displayed on the top "tab" on top of the tree) */
+ private String mTitle;
+ /** The description for the master-detail part (displayed on top of the tree view) */
+ private String mDescription;
+ /** The master-detail part, composed of a main tree and an auxiliary detail part */
+ private ManifestSectionPart mMasterPart;
+ /** The tree viewer in the master-detail part */
+ private TreeViewer mTreeViewer;
+ /** The "add" button for the tree view */
+ private Button mAddButton;
+ /** The "remove" button for the tree view */
+ private Button mRemoveButton;
+ /** The "up" button for the tree view */
+ private Button mUpButton;
+ /** The "down" button for the tree view */
+ private Button mDownButton;
+ /** The Managed Form used to create the master part */
+ private IManagedForm mManagedForm;
+ /** Reference to the details part of the tree master block. */
+ private DetailsPart mDetailsPart;
+ /** Reference to the clipboard for copy-paste */
+ private Clipboard mClipboard;
+ /** Listener to refresh the tree viewer when the parent's node has been updated */
+ private IUiUpdateListener mUiRefreshListener;
+ /** Listener to enable/disable the UI based on the application node's presence */
+ private IUiUpdateListener mUiEnableListener;
+ /** An adapter/wrapper to use the add/remove/up/down tree edit actions. */
+ private UiTreeActions mUiTreeActions;
+ /**
+ * True if the root node can be created on-demand (i.e. as needed as
+ * soon as children exist). False if an external entity controls the existence of the
+ * root node. In practise, this is false for the manifest application page (the actual
+ * "application" node is managed by the ApplicationToggle part) whereas it is true
+ * for all other tree pages.
+ */
+ private final boolean mAutoCreateRoot;
+
+
+ /**
+ * Creates a new {@link MasterDetailsBlock} that will display all UI nodes matching the
+ * given filter in the given root node.
+ *
+ * @param editor The parent manifest editor.
+ * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are
+ * to be manipulated by this tree view. In general this is the manifest UI node or the
+ * application UI node. This cannot be null.
+ * @param autoCreateRoot True if the root node can be created on-demand (i.e. as needed as
+ * soon as children exist). False if an external entity controls the existence of the
+ * root node. In practise, this is false for the manifest application page (the actual
+ * "application" node is managed by the ApplicationToggle part) whereas it is true
+ * for all other tree pages.
+ * @param descriptorFilters A list of descriptors of the elements to be displayed as root in
+ * this tree view. Use null or an empty list to accept any kind of node.
+ * @param title Title for the section
+ * @param description Description for the section
+ */
+ public UiTreeBlock(AndroidXmlEditor editor,
+ UiElementNode uiRootNode,
+ boolean autoCreateRoot,
+ ElementDescriptor[] descriptorFilters,
+ String title,
+ String description) {
+ mEditor = editor;
+ mUiRootNode = uiRootNode;
+ mAutoCreateRoot = autoCreateRoot;
+ mDescriptorFilters = descriptorFilters;
+ mTitle = title;
+ mDescription = description;
+ }
+
+ /** @returns The container editor */
+ AndroidXmlEditor getEditor() {
+ return mEditor;
+ }
+
+ /** @returns The reference to the clipboard for copy-paste */
+ Clipboard getClipboard() {
+ return mClipboard;
+ }
+
+ /** @returns The master-detail part, composed of a main tree and an auxiliary detail part */
+ ManifestSectionPart getMasterPart() {
+ return mMasterPart;
+ }
+
+ /**
+ * Returns the {@link UiElementNode} for the current model.
+ * <p/>
+ * This is used by the content provider attached to {@link #mTreeViewer} since
+ * the uiRootNode changes after each call to
+ * {@link #changeRootAndDescriptors(UiElementNode, ElementDescriptor[], boolean)}.
+ */
+ public UiElementNode getRootNode() {
+ return mUiRootNode;
+ }
+
+ @Override
+ protected void createMasterPart(final IManagedForm managedForm, Composite parent) {
+ FormToolkit toolkit = managedForm.getToolkit();
+
+ mManagedForm = managedForm;
+ mMasterPart = new ManifestSectionPart(parent, toolkit);
+ Section section = mMasterPart.getSection();
+ section.setText(mTitle);
+ section.setDescription(mDescription);
+ section.setLayout(new GridLayout());
+ section.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ Composite grid = SectionHelper.createGridLayout(section, toolkit, 2);
+
+ Tree tree = createTreeViewer(toolkit, grid, managedForm);
+ createButtons(toolkit, grid);
+ createTreeContextMenu(tree);
+ createSectionActions(section, toolkit);
+ }
+
+ private void createSectionActions(Section section, FormToolkit toolkit) {
+ ToolBarManager manager = new ToolBarManager(SWT.FLAT);
+ manager.removeAll();
+
+ ToolBar toolbar = manager.createControl(section);
+ section.setTextClient(toolbar);
+
+ ElementDescriptor[] descs = mDescriptorFilters;
+ if (descs == null && mUiRootNode != null) {
+ descs = mUiRootNode.getDescriptor().getChildren();
+ }
+
+ if (descs != null && descs.length > 1) {
+ for (ElementDescriptor desc : descs) {
+ manager.add(new DescriptorFilterAction(desc));
+ }
+ }
+
+ manager.add(new TreeSortAction());
+
+ manager.update(true /*force*/);
+ }
+
+ /**
+ * Creates the tree and its viewer
+ * @return The tree control
+ */
+ private Tree createTreeViewer(FormToolkit toolkit, Composite grid,
+ final IManagedForm managedForm) {
+ // Note: we *could* use a FilteredTree instead of the Tree+TreeViewer here.
+ // However the class must be adapted to create an adapted toolkit tree.
+ final Tree tree = toolkit.createTree(grid, SWT.MULTI);
+ GridData gd = new GridData(GridData.FILL_BOTH);
+ gd.widthHint = AndroidXmlEditor.TEXT_WIDTH_HINT;
+ gd.heightHint = TREE_HEIGHT_HINT;
+ tree.setLayoutData(gd);
+
+ mTreeViewer = new TreeViewer(tree);
+ mTreeViewer.setContentProvider(new UiModelTreeContentProvider(mUiRootNode, mDescriptorFilters));
+ mTreeViewer.setLabelProvider(new UiModelTreeLabelProvider());
+ mTreeViewer.setInput("unused"); //$NON-NLS-1$
+
+ // Create a listener that reacts to selections on the tree viewer.
+ // When a selection is made, ask the managed form to propagate an event to
+ // all parts in the managed form.
+ // This is picked up by UiElementDetail.selectionChanged().
+ mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ @Override
+ public void selectionChanged(SelectionChangedEvent event) {
+ managedForm.fireSelectionChanged(mMasterPart, event.getSelection());
+ adjustTreeButtons(event.getSelection());
+ }
+ });
+
+ // Create three listeners:
+ // - One to refresh the tree viewer when the parent's node has been updated
+ // - One to refresh the tree viewer when the framework resources have changed
+ // - One to enable/disable the UI based on the application node's presence.
+ mUiRefreshListener = new IUiUpdateListener() {
+ @Override
+ public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
+ mTreeViewer.refresh();
+ }
+ };
+
+ mUiEnableListener = new IUiUpdateListener() {
+ @Override
+ public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
+ // The UiElementNode for the application XML node always exists, even
+ // if there is no corresponding XML node in the XML file.
+ //
+ // Normally, we enable the UI here if the XML node is not null.
+ //
+ // However if mAutoCreateRoot is true, the root node will be created on-demand
+ // so the tree/block is always enabled.
+ boolean exists = mAutoCreateRoot || (ui_node.getXmlNode() != null);
+ if (mMasterPart != null) {
+ Section section = mMasterPart.getSection();
+ if (section.getEnabled() != exists) {
+ section.setEnabled(exists);
+ for (Control c : section.getChildren()) {
+ c.setEnabled(exists);
+ }
+ }
+ }
+ }
+ };
+
+ /** Listener to update the root node if the target of the file is changed because of a
+ * SDK location change or a project target change */
+ final ITargetChangeListener targetListener = new TargetChangeListener() {
+ @Override
+ public IProject getProject() {
+ if (mEditor != null) {
+ return mEditor.getProject();
+ }
+
+ return null;
+ }
+
+ @Override
+ public void reload() {
+ // If a details part has been created, we need to "refresh" it too.
+ if (mDetailsPart != null) {
+ // The details part does not directly expose access to its internal
+ // page book. Instead it is possible to resize the page book to 0 and then
+ // back to its original value, which has the side effect of removing all
+ // existing cached pages.
+ int limit = mDetailsPart.getPageLimit();
+ mDetailsPart.setPageLimit(0);
+ mDetailsPart.setPageLimit(limit);
+ }
+ // Refresh the tree, preserving the selection if possible.
+ mTreeViewer.refresh();
+ }
+ };
+
+ // Setup the listeners
+ changeRootAndDescriptors(mUiRootNode, mDescriptorFilters, false /* refresh */);
+
+ // Listen on resource framework changes to refresh the tree
+ AdtPlugin.getDefault().addTargetListener(targetListener);
+
+ // Remove listeners when the tree widget gets disposed.
+ tree.addDisposeListener(new DisposeListener() {
+ @Override
+ public void widgetDisposed(DisposeEvent e) {
+ if (mUiRootNode != null) {
+ UiElementNode node = mUiRootNode.getUiParent() != null ?
+ mUiRootNode.getUiParent() :
+ mUiRootNode;
+
+ if (node != null) {
+ node.removeUpdateListener(mUiRefreshListener);
+ }
+ mUiRootNode.removeUpdateListener(mUiEnableListener);
+ }
+
+ AdtPlugin.getDefault().removeTargetListener(targetListener);
+ if (mClipboard != null) {
+ mClipboard.dispose();
+ mClipboard = null;
+ }
+ }
+ });
+
+ // Get a new clipboard reference. It is disposed when the tree is disposed.
+ mClipboard = new Clipboard(tree.getDisplay());
+
+ return tree;
+ }
+
+ /**
+ * Changes the UI root node and the descriptor filters of the tree.
+ * <p/>
+ * This removes the listeners attached to the old root node and reattaches them to the
+ * new one.
+ *
+ * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are
+ * to be manipulated by this tree view. In general this is the manifest UI node or the
+ * application UI node. This cannot be null.
+ * @param descriptorFilters A list of descriptors of the elements to be displayed as root in
+ * this tree view. Use null or an empty list to accept any kind of node.
+ * @param forceRefresh If tree, forces the tree to refresh
+ */
+ public void changeRootAndDescriptors(UiElementNode uiRootNode,
+ ElementDescriptor[] descriptorFilters, boolean forceRefresh) {
+ UiElementNode node;
+
+ // Remove previous listeners if any
+ if (mUiRootNode != null) {
+ node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode;
+ node.removeUpdateListener(mUiRefreshListener);
+ mUiRootNode.removeUpdateListener(mUiEnableListener);
+ }
+
+ mUiRootNode = uiRootNode;
+ mDescriptorFilters = descriptorFilters;
+
+ mTreeViewer.setContentProvider(
+ new UiModelTreeContentProvider(mUiRootNode, mDescriptorFilters));
+
+ // Listen on structural changes on the root node of the tree
+ // If the node has a parent, listen on the parent instead.
+ if (mUiRootNode != null) {
+ node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode;
+
+ if (node != null) {
+ node.addUpdateListener(mUiRefreshListener);
+ }
+
+ // Use the root node to listen to its presence.
+ mUiRootNode.addUpdateListener(mUiEnableListener);
+
+ // Initialize the enabled/disabled state
+ mUiEnableListener.uiElementNodeUpdated(mUiRootNode, null /* state, not used */);
+ }
+
+ if (forceRefresh) {
+ mTreeViewer.refresh();
+ }
+
+ createSectionActions(mMasterPart.getSection(), mManagedForm.getToolkit());
+ }
+
+ /**
+ * Creates the buttons next to the tree.
+ */
+ private void createButtons(FormToolkit toolkit, Composite grid) {
+
+ mUiTreeActions = new UiTreeActions();
+
+ Composite button_grid = SectionHelper.createGridLayout(grid, toolkit, 1);
+ button_grid.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING));
+ mAddButton = toolkit.createButton(button_grid, "Add...", SWT.PUSH);
+ SectionHelper.addControlTooltip(mAddButton, "Adds a new element.");
+ mAddButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL |
+ GridData.VERTICAL_ALIGN_BEGINNING));
+
+ mAddButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ doTreeAdd();
+ }
+ });
+
+ mRemoveButton = toolkit.createButton(button_grid, "Remove...", SWT.PUSH);
+ SectionHelper.addControlTooltip(mRemoveButton, "Removes an existing selected element.");
+ mRemoveButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mRemoveButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ doTreeRemove();
+ }
+ });
+
+ mUpButton = toolkit.createButton(button_grid, "Up", SWT.PUSH);
+ SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element up.");
+ mUpButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mUpButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ doTreeUp();
+ }
+ });
+
+ mDownButton = toolkit.createButton(button_grid, "Down", SWT.PUSH);
+ SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element down.");
+ mDownButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mDownButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ doTreeDown();
+ }
+ });
+
+ adjustTreeButtons(TreeSelection.EMPTY);
+ }
+
+ private void createTreeContextMenu(Tree tree) {
+ MenuManager menuManager = new MenuManager();
+ menuManager.setRemoveAllWhenShown(true);
+ menuManager.addMenuListener(new IMenuListener() {
+ /**
+ * The menu is about to be shown. The menu manager has already been
+ * requested to remove any existing menu item. This method gets the
+ * tree selection and if it is of the appropriate type it re-creates
+ * the necessary actions.
+ */
+ @Override
+ public void menuAboutToShow(IMenuManager manager) {
+ ISelection selection = mTreeViewer.getSelection();
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ doCreateMenuAction(manager, selected);
+ return;
+ }
+ doCreateMenuAction(manager, null /* ui_node */);
+ }
+ });
+ Menu contextMenu = menuManager.createContextMenu(tree);
+ tree.setMenu(contextMenu);
+ }
+
+ /**
+ * Adds the menu actions to the context menu when the given UI node is selected in
+ * the tree view.
+ *
+ * @param manager The context menu manager
+ * @param selected The UI nodes selected in the tree. Can be null, in which case the root
+ * is to be modified.
+ */
+ private void doCreateMenuAction(IMenuManager manager, ArrayList<UiElementNode> selected) {
+ if (selected != null) {
+ boolean hasXml = false;
+ for (UiElementNode uiNode : selected) {
+ if (uiNode.getXmlNode() != null) {
+ hasXml = true;
+ break;
+ }
+ }
+
+ if (hasXml) {
+ manager.add(new CopyCutAction(getEditor(), getClipboard(),
+ null, selected, true /* cut */));
+ manager.add(new CopyCutAction(getEditor(), getClipboard(),
+ null, selected, false /* cut */));
+
+ // Can't paste with more than one element selected (the selection is the target)
+ if (selected.size() <= 1) {
+ // Paste is not valid if it would add a second element on a terminal element
+ // which parent is a document -- an XML document can only have one child. This
+ // means paste is valid if the current UI node can have children or if the
+ // parent is not a document.
+ UiElementNode ui_root = selected.get(0).getUiRoot();
+ if (ui_root.getDescriptor().hasChildren() ||
+ !(ui_root.getUiParent() instanceof UiDocumentNode)) {
+ manager.add(new PasteAction(getEditor(), getClipboard(), selected.get(0)));
+ }
+ }
+ manager.add(new Separator());
+ }
+ }
+
+ // Append "add" and "remove" actions. They do the same thing as the add/remove
+ // buttons on the side.
+ IconFactory factory = IconFactory.getInstance();
+
+ // "Add" makes sense only if there's 0 or 1 item selected since the
+ // one selected item becomes the target.
+ if (selected == null || selected.size() <= 1) {
+ manager.add(new Action("Add...", factory.getImageDescriptor("add")) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ super.run();
+ doTreeAdd();
+ }
+ });
+ }
+
+ if (selected != null) {
+ if (selected != null) {
+ manager.add(new Action("Remove", factory.getImageDescriptor("delete")) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ super.run();
+ doTreeRemove();
+ }
+ });
+ }
+ manager.add(new Separator());
+
+ manager.add(new Action("Up", factory.getImageDescriptor("up")) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ super.run();
+ doTreeUp();
+ }
+ });
+ manager.add(new Action("Down", factory.getImageDescriptor("down")) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ super.run();
+ doTreeDown();
+ }
+ });
+ }
+ }
+
+
+ /**
+ * This is called by the tree when a selection is made.
+ * It enables/disables the buttons associated with the tree depending on the current
+ * selection.
+ *
+ * @param selection The current tree selection (same as mTreeViewer.getSelection())
+ */
+ private void adjustTreeButtons(ISelection selection) {
+ mRemoveButton.setEnabled(!selection.isEmpty() && selection instanceof ITreeSelection);
+ mUpButton.setEnabled(canDoTreeUp(selection));
+ mDownButton.setEnabled(canDoTreeDown(selection));
+ }
+
+ /**
+ * An adapter/wrapper to use the add/remove/up/down tree edit actions.
+ */
+ private class UiTreeActions extends UiActions {
+ @Override
+ protected UiElementNode getRootNode() {
+ return mUiRootNode;
+ }
+
+ @Override
+ protected void selectUiNode(UiElementNode uiNodeToSelect) {
+ // Select the new item
+ if (uiNodeToSelect != null) {
+ LinkedList<UiElementNode> segments = new LinkedList<UiElementNode>();
+ for (UiElementNode ui_node = uiNodeToSelect; ui_node != mUiRootNode;
+ ui_node = ui_node.getUiParent()) {
+ segments.add(0, ui_node);
+ }
+ if (segments.size() > 0) {
+ mTreeViewer.setSelection(new TreeSelection(new TreePath(segments.toArray())));
+ } else {
+ mTreeViewer.setSelection(null);
+ }
+ }
+ }
+
+ @Override
+ public void commitPendingXmlChanges() {
+ commitManagedForm();
+ }
+ }
+
+ /**
+ * Filters an ITreeSelection to only keep the {@link UiElementNode}s (in case there's
+ * something else in there).
+ *
+ * @return A new list of {@link UiElementNode} with at least one item or null.
+ */
+ private ArrayList<UiElementNode> filterSelection(ITreeSelection selection) {
+ ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>();
+
+ for (Iterator<Object> it = selection.iterator(); it.hasNext(); ) {
+ Object selectedObj = it.next();
+
+ if (selectedObj instanceof UiElementNode) {
+ selected.add((UiElementNode) selectedObj);
+ }
+ }
+
+ return selected.size() > 0 ? selected : null;
+ }
+
+ /**
+ * 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.
+ */
+ private void doTreeAdd() {
+ UiElementNode ui_node = mUiRootNode;
+ ISelection selection = mTreeViewer.getSelection();
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ITreeSelection tree_selection = (ITreeSelection) selection;
+ Object first = tree_selection.getFirstElement();
+ if (first != null && first instanceof UiElementNode) {
+ ui_node = (UiElementNode) first;
+ }
+ }
+
+ mUiTreeActions.doAdd(
+ ui_node,
+ mDescriptorFilters,
+ mTreeViewer.getControl().getShell(),
+ (ILabelProvider) mTreeViewer.getLabelProvider());
+ }
+
+ /**
+ * 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.
+ */
+ protected void doTreeRemove() {
+ ISelection selection = mTreeViewer.getSelection();
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ mUiTreeActions.doRemove(selected, mTreeViewer.getControl().getShell());
+ }
+ }
+
+ /**
+ * 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.
+ */
+ protected void doTreeUp() {
+ ISelection selection = mTreeViewer.getSelection();
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ mUiTreeActions.doUp(selected, mDescriptorFilters);
+ }
+ }
+
+ /**
+ * Checks whether the "up" action can be done on the current selection.
+ *
+ * @param selection The current tree selection.
+ * @return True if all the selected nodes can be moved up.
+ */
+ protected boolean canDoTreeUp(ISelection selection) {
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ return mUiTreeActions.canDoUp(selected, mDescriptorFilters);
+ }
+
+ 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.
+ */
+ protected void doTreeDown() {
+ ISelection selection = mTreeViewer.getSelection();
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ mUiTreeActions.doDown(selected, mDescriptorFilters);
+ }
+ }
+
+ /**
+ * Checks whether the "down" action can be done on the current selection.
+ *
+ * @param selection The current tree selection.
+ * @return True if all the selected nodes can be moved down.
+ */
+ protected boolean canDoTreeDown(ISelection selection) {
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ return mUiTreeActions.canDoDown(selected, mDescriptorFilters);
+ }
+
+ return false;
+ }
+
+ /**
+ * Commits the current managed form (the one associated with our master part).
+ * As a side effect, this will commit the current UiElementDetails page.
+ */
+ void commitManagedForm() {
+ if (mManagedForm != null) {
+ mManagedForm.commit(false /* onSave */);
+ }
+ }
+
+ /* Implements ICommitXml for CopyCutAction */
+ @Override
+ public void commitPendingXmlChanges() {
+ commitManagedForm();
+ }
+
+ @Override
+ protected void createToolBarActions(IManagedForm managedForm) {
+ // Pass. Not used, toolbar actions are defined by createSectionActions().
+ }
+
+ @Override
+ protected void registerPages(DetailsPart inDetailsPart) {
+ // Keep a reference on the details part (the super class doesn't provide a getter
+ // for it.)
+ mDetailsPart = inDetailsPart;
+
+ // The page selection mechanism does not use pages registered by association with
+ // a node class. Instead it uses a custom details page provider that provides a
+ // new UiElementDetail instance for each node instance. A limit of 5 pages is
+ // then set (the value is arbitrary but should be reasonable) for the internal
+ // page book.
+ inDetailsPart.setPageLimit(5);
+
+ final UiTreeBlock tree = this;
+
+ inDetailsPart.setPageProvider(new IDetailsPageProvider() {
+ @Override
+ public IDetailsPage getPage(Object key) {
+ if (key instanceof UiElementNode) {
+ return new UiElementDetail(tree);
+ }
+ return null;
+ }
+
+ @Override
+ public Object getPageKey(Object object) {
+ return object; // use node object as key
+ }
+ });
+ }
+
+ /**
+ * An alphabetic sort action for the tree viewer.
+ */
+ private class TreeSortAction extends Action {
+
+ private ViewerComparator mComparator;
+
+ public TreeSortAction() {
+ super("Sorts elements alphabetically.", AS_CHECK_BOX);
+ setImageDescriptor(IconFactory.getInstance().getImageDescriptor("az_sort")); //$NON-NLS-1$
+
+ if (mTreeViewer != null) {
+ boolean is_sorted = mTreeViewer.getComparator() != null;
+ setChecked(is_sorted);
+ }
+ }
+
+ /**
+ * Called when the button is selected. Toggles the tree viewer comparator.
+ */
+ @Override
+ public void run() {
+ if (mTreeViewer == null) {
+ notifyResult(false /*success*/);
+ return;
+ }
+
+ ViewerComparator comp = mTreeViewer.getComparator();
+ if (comp != null) {
+ // Tree is currently sorted.
+ // Save currently comparator and remove it
+ mComparator = comp;
+ mTreeViewer.setComparator(null);
+ } else {
+ // Tree is not currently sorted.
+ // Reuse or add a new comparator.
+ if (mComparator == null) {
+ mComparator = new ViewerComparator();
+ }
+ mTreeViewer.setComparator(mComparator);
+ }
+
+ notifyResult(true /*success*/);
+ }
+ }
+
+ /**
+ * A filter on descriptor for the tree viewer.
+ * <p/>
+ * The tree viewer will contain many of these actions and only one can be enabled at a
+ * given time. When no action is selected, everything is displayed.
+ * <p/>
+ * Since "radio"-like actions do not allow for unselecting all of them, we manually
+ * handle the exclusive radio button-like property: when an action is selected, it manually
+ * removes all other actions as needed.
+ */
+ private class DescriptorFilterAction extends Action {
+
+ private final ElementDescriptor mDescriptor;
+ private ViewerFilter mFilter;
+
+ public DescriptorFilterAction(ElementDescriptor descriptor) {
+ super(String.format("Displays only %1$s elements.", descriptor.getUiName()),
+ AS_CHECK_BOX);
+
+ mDescriptor = descriptor;
+ setImageDescriptor(descriptor.getImageDescriptor());
+ }
+
+ /**
+ * Called when the button is selected.
+ * <p/>
+ * Find any existing {@link DescriptorFilter}s and remove them. Install ours.
+ */
+ @Override
+ public void run() {
+ super.run();
+
+ if (isChecked()) {
+ if (mFilter == null) {
+ // create filter when required
+ mFilter = new DescriptorFilter(this);
+ }
+
+ // we add our filter first, otherwise the UI might show the full list
+ mTreeViewer.addFilter(mFilter);
+
+ // Then remove the any other filters except ours. There should be at most
+ // one other filter, since that's how the actions are made to look like
+ // exclusive radio buttons.
+ for (ViewerFilter filter : mTreeViewer.getFilters()) {
+ if (filter instanceof DescriptorFilter && filter != mFilter) {
+ DescriptorFilterAction action = ((DescriptorFilter) filter).getAction();
+ action.setChecked(false);
+ mTreeViewer.removeFilter(filter);
+ }
+ }
+ } else if (mFilter != null){
+ mTreeViewer.removeFilter(mFilter);
+ }
+ }
+
+ /**
+ * Filters the tree viewer for the given descriptor.
+ * <p/>
+ * The filter is linked to the action so that an action can iterate through the list
+ * of filters and un-select the actions.
+ */
+ private class DescriptorFilter extends ViewerFilter {
+
+ private final DescriptorFilterAction mAction;
+
+ public DescriptorFilter(DescriptorFilterAction action) {
+ mAction = action;
+ }
+
+ public DescriptorFilterAction getAction() {
+ return mAction;
+ }
+
+ /**
+ * Returns true if an element should be displayed, that if the element or
+ * any of its parent matches the requested descriptor.
+ */
+ @Override
+ public boolean select(Viewer viewer, Object parentElement, Object element) {
+ while (element instanceof UiElementNode) {
+ UiElementNode uiNode = (UiElementNode)element;
+ if (uiNode.getDescriptor() == mDescriptor) {
+ return true;
+ }
+ element = uiNode.getUiParent();
+ }
+ return false;
+ }
+ }
+ }
+
+}