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