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