Home | History | Annotate | Download | only in tree
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.editors.ui.tree;
     18 
     19 import com.android.ide.eclipse.adt.AdtPlugin;
     20 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     21 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
     22 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
     23 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     24 
     25 import org.eclipse.core.resources.IFile;
     26 import org.eclipse.core.runtime.IStatus;
     27 import org.eclipse.core.runtime.Status;
     28 import org.eclipse.jface.viewers.ILabelProvider;
     29 import org.eclipse.swt.SWT;
     30 import org.eclipse.swt.events.SelectionAdapter;
     31 import org.eclipse.swt.events.SelectionEvent;
     32 import org.eclipse.swt.widgets.Button;
     33 import org.eclipse.swt.widgets.Composite;
     34 import org.eclipse.swt.widgets.Control;
     35 import org.eclipse.swt.widgets.Shell;
     36 import org.eclipse.ui.IEditorInput;
     37 import org.eclipse.ui.dialogs.AbstractElementListSelectionDialog;
     38 import org.eclipse.ui.dialogs.ISelectionStatusValidator;
     39 import org.eclipse.ui.part.FileEditorInput;
     40 
     41 import java.util.Arrays;
     42 import java.util.HashMap;
     43 import java.util.Map;
     44 import java.util.TreeMap;
     45 import java.util.Map.Entry;
     46 
     47 /**
     48  * A selection dialog to select the type of the new element node to
     49  * create, either in the application node or the selected sub node.
     50  */
     51 public class NewItemSelectionDialog extends AbstractElementListSelectionDialog {
     52 
     53     /** The UI node selected in the tree view before creating the new item selection dialog.
     54      *  Can be null -- which means new items must be created in the root_node. */
     55     private UiElementNode mSelectedUiNode;
     56     /** The root node chosen by the user, either root_node or the one passed
     57      *  to the constructor if not null */
     58     private UiElementNode mChosenRootNode;
     59     private UiElementNode mLocalRootNode;
     60     /** The descriptor of the elements to be displayed as root in this tree view. All elements
     61      *  of the same type in the root will be displayed. Can be null. */
     62     private ElementDescriptor[] mDescriptorFilters;
     63     /** The key for the {@link #setLastUsedXmlName(Object[])}. It corresponds to the full
     64      * workspace path of the currently edited file, if this can be computed. This is computed
     65      * by {@link #getLastUsedXmlName(UiElementNode)}, called from the constructor. */
     66     private String mLastUsedKey;
     67     /** A static map of known XML Names used for a given file. The map has full workspace
     68      * paths as key and XML names as values. */
     69     private static final Map<String, String> sLastUsedXmlName = new HashMap<String, String>();
     70     /** The potential XML Name to initially select in the selection dialog. This is computed
     71      * in the constructor and set by {@link #setInitialSelection(UiElementNode)}. */
     72     private String mInitialXmlName;
     73 
     74     /**
     75      * Creates the new item selection dialog.
     76      *
     77      * @param shell The parent shell for the list.
     78      * @param labelProvider ILabelProvider for the list.
     79      * @param descriptorFilters The element allows at the root of the tree. Can be null.
     80      * @param ui_node The selected node, or null if none is selected.
     81      * @param root_node The root of the Ui Tree, either the UiDocumentNode or a sub-node.
     82      */
     83     public NewItemSelectionDialog(Shell shell, ILabelProvider labelProvider,
     84             ElementDescriptor[] descriptorFilters,
     85             UiElementNode ui_node,
     86             UiElementNode root_node) {
     87         super(shell, labelProvider);
     88         mDescriptorFilters = descriptorFilters;
     89         mLocalRootNode = root_node;
     90 
     91         // Only accept the UI node if it is not the UI root node and it can have children.
     92         // If the node cannot have children, select its parent as a potential target.
     93         if (ui_node != null && ui_node != mLocalRootNode) {
     94             if (ui_node.getDescriptor().hasChildren()) {
     95                 mSelectedUiNode = ui_node;
     96             } else {
     97                 UiElementNode parent = ui_node.getUiParent();
     98                 if (parent != null && parent != mLocalRootNode) {
     99                     mSelectedUiNode = parent;
    100                 }
    101             }
    102         }
    103 
    104         setHelpAvailable(false);
    105         setMultipleSelection(false);
    106 
    107         setValidator(new ISelectionStatusValidator() {
    108             public IStatus validate(Object[] selection) {
    109                 if (selection.length == 1 && selection[0] instanceof ViewElementDescriptor) {
    110                     return new Status(IStatus.OK, // severity
    111                             AdtPlugin.PLUGIN_ID, //plugin id
    112                             IStatus.OK, // code
    113                             ((ViewElementDescriptor) selection[0]).getFullClassName(), //msg
    114                             null); // exception
    115                 } else if (selection.length == 1 && selection[0] instanceof ElementDescriptor) {
    116                     return new Status(IStatus.OK, // severity
    117                             AdtPlugin.PLUGIN_ID, //plugin id
    118                             IStatus.OK, // code
    119                             "", //$NON-NLS-1$ // msg
    120                             null); // exception
    121                 } else {
    122                     return new Status(IStatus.ERROR, // severity
    123                             AdtPlugin.PLUGIN_ID, //plugin id
    124                             IStatus.ERROR, // code
    125                             "Invalid selection", // msg, translatable
    126                             null); // exception
    127                 }
    128             }
    129         });
    130 
    131         // Determine the initial selection using a couple heuristics.
    132 
    133         // First check if we can get the last used node type for this file.
    134         // The heuristic is that generally one keeps adding the same kind of items to the
    135         // same file, so reusing the last used item type makes most sense.
    136         String xmlName = getLastUsedXmlName(root_node);
    137         if (xmlName == null) {
    138             // Another heuristic is to find the most used item and default to that.
    139             xmlName = getMostUsedXmlName(root_node);
    140         }
    141         if (xmlName == null) {
    142             // Finally the last heuristic is to see if there's an item with a name
    143             // similar to the edited file name.
    144             xmlName = getLeafFileName(root_node);
    145         }
    146         // Set the potential name. Selecting the right item is done later by setInitialSelection().
    147         mInitialXmlName = xmlName;
    148     }
    149 
    150     /**
    151      * Returns a potential XML name based on the file name.
    152      * The item name is marked with an asterisk to identify it as a partial match.
    153      */
    154     private String getLeafFileName(UiElementNode ui_node) {
    155         if (ui_node != null) {
    156             AndroidXmlEditor editor = ui_node.getEditor();
    157             if (editor != null) {
    158                 IEditorInput editorInput = editor.getEditorInput();
    159                 if (editorInput instanceof FileEditorInput) {
    160                     IFile f = ((FileEditorInput) editorInput).getFile();
    161                     if (f != null) {
    162                         String leafName = f.getFullPath().removeFileExtension().lastSegment();
    163                         return "*" + leafName; //$NON-NLS-1$
    164                     }
    165                 }
    166             }
    167         }
    168 
    169         return null;
    170     }
    171 
    172     /**
    173      * Given a potential non-null root node, this method looks for the currently edited
    174      * file path and uses it as a key to retrieve the last used item for this file by this
    175      * selection dialog. Returns null if nothing can be found, otherwise returns the string
    176      * name of the item.
    177      */
    178     private String getLastUsedXmlName(UiElementNode ui_node) {
    179         if (ui_node != null) {
    180             AndroidXmlEditor editor = ui_node.getEditor();
    181             if (editor != null) {
    182                 IEditorInput editorInput = editor.getEditorInput();
    183                 if (editorInput instanceof FileEditorInput) {
    184                     IFile f = ((FileEditorInput) editorInput).getFile();
    185                     if (f != null) {
    186                         mLastUsedKey = f.getFullPath().toPortableString();
    187 
    188                         return sLastUsedXmlName.get(mLastUsedKey);
    189                     }
    190                 }
    191             }
    192         }
    193 
    194         return null;
    195     }
    196 
    197     /**
    198      * Sets the last used item for this selection dialog for this file.
    199      * @param objects The currently selected items. Only the first one is used if it is an
    200      *                {@link ElementDescriptor}.
    201      */
    202     private void setLastUsedXmlName(Object[] objects) {
    203         if (mLastUsedKey != null &&
    204                 objects != null &&
    205                 objects.length > 0 &&
    206                 objects[0] instanceof ElementDescriptor) {
    207             ElementDescriptor desc = (ElementDescriptor) objects[0];
    208             sLastUsedXmlName.put(mLastUsedKey, desc.getXmlName());
    209         }
    210     }
    211 
    212     /**
    213      * Returns the most used sub-element name, if any, or null.
    214      */
    215     private String getMostUsedXmlName(UiElementNode ui_node) {
    216         if (ui_node != null) {
    217             TreeMap<String, Integer> counts = new TreeMap<String, Integer>();
    218             int max = -1;
    219 
    220             for (UiElementNode child : ui_node.getUiChildren()) {
    221                 String name = child.getDescriptor().getXmlName();
    222                 Integer i = counts.get(name);
    223                 int count = i == null ? 1 : i.intValue() + 1;
    224                 counts.put(name, count);
    225                 max = Math.max(max, count);
    226             }
    227 
    228             if (max > 0) {
    229                 // Find first key with this max and return it
    230                 for (Entry<String, Integer> entry : counts.entrySet()) {
    231                     if (entry.getValue().intValue() == max) {
    232                         return entry.getKey();
    233                     }
    234                 }
    235             }
    236         }
    237         return null;
    238     }
    239 
    240     /**
    241      * @return The root node selected by the user, either root node or the
    242      *         one passed to the constructor if not null.
    243      */
    244     public UiElementNode getChosenRootNode() {
    245         return mChosenRootNode;
    246     }
    247 
    248     /**
    249      * Internal helper to compute the result. Returns the selection from
    250      * the list view, if any.
    251      */
    252     @Override
    253     protected void computeResult() {
    254         setResult(Arrays.asList(getSelectedElements()));
    255         setLastUsedXmlName(getSelectedElements());
    256     }
    257 
    258     /**
    259      * Creates the dialog area.
    260      *
    261      * First add a radio area, which may be either 2 radio controls or
    262      * just a message area if there's only one choice (the app root node).
    263      *
    264      * Then uses the default from the AbstractElementListSelectionDialog
    265      * which is to add both a filter text and a filtered list. Adding both
    266      * is necessary (since the base class accesses both internal directly
    267      * fields without checking for null pointers.)
    268      *
    269      * Finally sets the initial selection list.
    270      */
    271     @Override
    272     protected Control createDialogArea(Composite parent) {
    273         Composite contents = (Composite) super.createDialogArea(parent);
    274 
    275         createRadioControl(contents);
    276         createFilterText(contents);
    277         createFilteredList(contents);
    278 
    279         // Initialize the list state.
    280         // This must be done after the filtered list as been created.
    281         chooseNode(mChosenRootNode);
    282 
    283         // Set the initial selection
    284         setInitialSelection(mChosenRootNode);
    285         return contents;
    286     }
    287 
    288     /**
    289      * Tries to set the initial selection based on the {@link #mInitialXmlName} computed
    290      * in the constructor. The selection is only set if there's an element descriptor
    291      * that matches the same exact XML name. When {@link #mInitialXmlName} starts with an
    292      * asterisk, it means to do a partial case-insensitive match on the start of the
    293      * strings.
    294      */
    295     private void setInitialSelection(UiElementNode rootNode) {
    296         ElementDescriptor initialElement = null;
    297 
    298         if (mInitialXmlName != null && mInitialXmlName.length() > 0) {
    299             String name = mInitialXmlName;
    300             boolean partial = name.startsWith("*");   //$NON-NLS-1$
    301             if (partial) {
    302                 name = name.substring(1).toLowerCase();
    303             }
    304 
    305             for (ElementDescriptor desc : getAllowedDescriptors(rootNode)) {
    306                 if (!partial && desc.getXmlName().equals(name)) {
    307                     initialElement = desc;
    308                     break;
    309                 } else if (partial) {
    310                     String name2 = desc.getXmlLocalName().toLowerCase();
    311                     if (name.startsWith(name2) || name2.startsWith(name)) {
    312                         initialElement = desc;
    313                         break;
    314                     }
    315                 }
    316             }
    317         }
    318 
    319         setSelection(initialElement == null ? null : new ElementDescriptor[] { initialElement });
    320     }
    321 
    322     /**
    323      * Creates the message text widget and sets layout data.
    324      * @param content the parent composite of the message area.
    325      */
    326     private Composite createRadioControl(Composite content) {
    327 
    328         if (mSelectedUiNode != null) {
    329             Button radio1 = new Button(content, SWT.RADIO);
    330             radio1.setText(String.format("Create a new element at the top level, in %1$s.",
    331                     mLocalRootNode.getShortDescription()));
    332 
    333             Button radio2 = new Button(content, SWT.RADIO);
    334             radio2.setText(String.format("Create a new element in the selected element, %1$s.",
    335                     mSelectedUiNode.getBreadcrumbTrailDescription(false /* include_root */)));
    336 
    337             // Set the initial selection before adding the listeners
    338             // (they can't be run till the filtered list has been created)
    339             radio1.setSelection(false);
    340             radio2.setSelection(true);
    341             mChosenRootNode = mSelectedUiNode;
    342 
    343             radio1.addSelectionListener(new SelectionAdapter() {
    344                 @Override
    345                 public void widgetSelected(SelectionEvent e) {
    346                     super.widgetSelected(e);
    347                     chooseNode(mLocalRootNode);
    348                 }
    349             });
    350 
    351             radio2.addSelectionListener(new SelectionAdapter() {
    352                 @Override
    353                 public void widgetSelected(SelectionEvent e) {
    354                     super.widgetSelected(e);
    355                     chooseNode(mSelectedUiNode);
    356                 }
    357             });
    358         } else {
    359             setMessage(String.format("Create a new element at the top level, in %1$s.",
    360                     mLocalRootNode.getShortDescription()));
    361             createMessageArea(content);
    362 
    363             mChosenRootNode = mLocalRootNode;
    364         }
    365 
    366         return content;
    367     }
    368 
    369     /**
    370      * Internal helper to remember the root node choosen by the user.
    371      * It also sets the list view to the adequate list of children that can
    372      * be added to the chosen root node.
    373      *
    374      * If the chosen root node is mLocalRootNode and a descriptor filter was specified
    375      * when creating the master-detail part, we use this as the set of nodes that
    376      * can be created on the root node.
    377      *
    378      * @param ui_node The chosen root node, either mLocalRootNode or
    379      *                mSelectedUiNode.
    380      */
    381     private void chooseNode(UiElementNode ui_node) {
    382         mChosenRootNode = ui_node;
    383         setListElements(getAllowedDescriptors(ui_node));
    384     }
    385 
    386     /**
    387      * Returns the list of {@link ElementDescriptor}s that can be added to the given
    388      * UI node.
    389      *
    390      * @param ui_node The UI node to which element should be added. Cannot be null.
    391      * @return A non-null array of {@link ElementDescriptor}. The array might be empty.
    392      */
    393     private ElementDescriptor[] getAllowedDescriptors(UiElementNode ui_node) {
    394         if (ui_node == mLocalRootNode &&
    395                 mDescriptorFilters != null &&
    396                 mDescriptorFilters.length != 0) {
    397             return mDescriptorFilters;
    398         } else {
    399             return ui_node.getDescriptor().getChildren();
    400         }
    401     }
    402 }
    403