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