blob: 36bd148f60b7dd27f5bf28921c521797e6f738df [file] [log] [blame]
/*
* 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.editors.ui.tree;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.editors.AndroidEditor;
import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
import com.android.ide.eclipse.editors.layout.descriptors.ViewElementDescriptor;
import com.android.ide.eclipse.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.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
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.Map;
import java.util.TreeMap;
import java.util.Map.Entry;
/**
* 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() {
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]).getCanonicalClassName(), //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) {
AndroidEditor 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) {
AndroidEditor 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);
// 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();
}
for (ElementDescriptor desc : getAllowedDescriptors(rootNode)) {
if (!partial && desc.getXmlName().equals(name)) {
initialElement = desc;
break;
} else if (partial) {
String name2 = desc.getXmlLocalName().toLowerCase();
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();
}
}
}