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