2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.eclipse.org/org/documents/epl-v10.php
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.
17 package com.android.ide.eclipse.adt.internal.editors.manifest.model;
19 import com.android.ide.eclipse.adt.AdtPlugin;
20 import com.android.ide.eclipse.adt.AndroidConstants;
21 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
22 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
23 import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
24 import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
25 import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
26 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
27 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiTextAttributeNode;
28 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
29 import com.android.sdklib.SdkConstants;
30 import com.android.sdklib.xml.AndroidManifest;
32 import org.eclipse.core.resources.IFile;
33 import org.eclipse.core.resources.IProject;
34 import org.eclipse.core.runtime.CoreException;
35 import org.eclipse.core.runtime.NullProgressMonitor;
36 import org.eclipse.jdt.core.Flags;
37 import org.eclipse.jdt.core.IClasspathEntry;
38 import org.eclipse.jdt.core.IJavaElement;
39 import org.eclipse.jdt.core.IJavaProject;
40 import org.eclipse.jdt.core.IPackageFragment;
41 import org.eclipse.jdt.core.IPackageFragmentRoot;
42 import org.eclipse.jdt.core.IType;
43 import org.eclipse.jdt.core.ITypeHierarchy;
44 import org.eclipse.jdt.core.JavaCore;
45 import org.eclipse.jdt.core.JavaModelException;
46 import org.eclipse.jdt.core.search.IJavaSearchScope;
47 import org.eclipse.jdt.core.search.SearchEngine;
48 import org.eclipse.jdt.ui.IJavaElementSearchConstants;
49 import org.eclipse.jdt.ui.JavaUI;
50 import org.eclipse.jdt.ui.actions.OpenNewClassWizardAction;
51 import org.eclipse.jdt.ui.dialogs.ITypeInfoFilterExtension;
52 import org.eclipse.jdt.ui.dialogs.ITypeInfoRequestor;
53 import org.eclipse.jdt.ui.dialogs.ITypeSelectionComponent;
54 import org.eclipse.jdt.ui.dialogs.TypeSelectionExtension;
55 import org.eclipse.jdt.ui.wizards.NewClassWizardPage;
56 import org.eclipse.jface.dialogs.IMessageProvider;
57 import org.eclipse.jface.window.Window;
58 import org.eclipse.swt.SWT;
59 import org.eclipse.swt.events.DisposeEvent;
60 import org.eclipse.swt.events.DisposeListener;
61 import org.eclipse.swt.events.ModifyEvent;
62 import org.eclipse.swt.events.ModifyListener;
63 import org.eclipse.swt.events.SelectionAdapter;
64 import org.eclipse.swt.events.SelectionEvent;
65 import org.eclipse.swt.layout.GridData;
66 import org.eclipse.swt.layout.GridLayout;
67 import org.eclipse.swt.widgets.Button;
68 import org.eclipse.swt.widgets.Composite;
69 import org.eclipse.swt.widgets.Control;
70 import org.eclipse.swt.widgets.Text;
71 import org.eclipse.ui.IEditorInput;
72 import org.eclipse.ui.IFileEditorInput;
73 import org.eclipse.ui.PartInitException;
74 import org.eclipse.ui.PlatformUI;
75 import org.eclipse.ui.dialogs.SelectionDialog;
76 import org.eclipse.ui.forms.IManagedForm;
77 import org.eclipse.ui.forms.events.HyperlinkAdapter;
78 import org.eclipse.ui.forms.events.HyperlinkEvent;
79 import org.eclipse.ui.forms.widgets.FormText;
80 import org.eclipse.ui.forms.widgets.FormToolkit;
81 import org.eclipse.ui.forms.widgets.TableWrapData;
82 import org.w3c.dom.Element;
84 import java.util.ArrayList;
87 * Represents an XML attribute for a class, that can be modified using a simple text field or
88 * a dialog to choose an existing class. Also, there's a link to create a new class.
90 * See {@link UiTextAttributeNode} for more information.
92 public class UiClassAttributeNode extends UiTextAttributeNode {
94 private String mReferenceClass;
95 private IPostTypeCreationAction mPostCreationAction;
96 private boolean mMandatory;
97 private final boolean mDefaultToProjectOnly;
99 private class HierarchyTypeSelection extends TypeSelectionExtension {
101 private IJavaProject mJavaProject;
102 private IType mReferenceType;
103 private Button mProjectOnly;
104 private boolean mUseProjectOnly;
106 public HierarchyTypeSelection(IProject project, String referenceClass)
107 throws JavaModelException {
108 mJavaProject = JavaCore.create(project);
109 mReferenceType = mJavaProject.findType(referenceClass);
113 public ITypeInfoFilterExtension getFilterExtension() {
114 return new ITypeInfoFilterExtension() {
115 public boolean select(ITypeInfoRequestor typeInfoRequestor) {
117 boolean projectOnly = mUseProjectOnly;
119 String packageName = typeInfoRequestor.getPackageName();
120 String typeName = typeInfoRequestor.getTypeName();
121 String enclosingType = typeInfoRequestor.getEnclosingName();
123 // build the full class name.
124 StringBuilder sb = new StringBuilder(packageName);
126 if (enclosingType.length() > 0) {
127 sb.append(enclosingType);
132 String className = sb.toString();
135 IType type = mJavaProject.findType(className);
141 // don't display abstract classes
142 if ((type.getFlags() & Flags.AccAbstract) != 0) {
146 // if project-only is selected, make sure the package fragment is
147 // an actual source (thus "from this project").
149 IPackageFragment frag = type.getPackageFragment();
150 if (frag == null || frag.getKind() != IPackageFragmentRoot.K_SOURCE) {
155 // get the type hierarchy and reference type is one of the super classes.
156 ITypeHierarchy hierarchy = type.newSupertypeHierarchy(
157 new NullProgressMonitor());
159 IType[] supertypes = hierarchy.getAllSupertypes(type);
160 int n = supertypes.length;
161 for (int i = 0; i < n; i++) {
162 IType st = supertypes[i];
163 if (mReferenceType.equals(st)) {
167 } catch (JavaModelException e) {
176 public Control createContentArea(Composite parent) {
178 mProjectOnly = new Button(parent, SWT.CHECK);
179 mProjectOnly.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
180 mProjectOnly.setText(String.format("Display classes from sources of project '%s' only",
181 mJavaProject.getProject().getName()));
183 mUseProjectOnly = mDefaultToProjectOnly;
184 mProjectOnly.setSelection(mUseProjectOnly);
186 mProjectOnly.addSelectionListener(new SelectionAdapter() {
188 public void widgetSelected(SelectionEvent e) {
189 super.widgetSelected(e);
190 mUseProjectOnly = mProjectOnly.getSelection();
191 getTypeSelectionComponent().triggerSearch();
195 return super.createContentArea(parent);
200 * Classes which implement this interface provide a method processing newly created classes.
202 public static interface IPostTypeCreationAction {
204 * Sent to process a newly created class.
205 * @param newType the IType representing the newly created class.
207 public void processNewType(IType newType);
211 * Creates a {@link UiClassAttributeNode} object that will display ui to select or create
213 * @param referenceClass The allowed supertype of the classes that are to be selected
214 * or created. Can be null.
215 * @param postCreationAction a {@link IPostTypeCreationAction} object handling post creation
216 * modification of the class.
217 * @param mandatory indicates if the class value is mandatory
218 * @param attributeDescriptor the {@link AttributeDescriptor} object linked to the Ui Node.
219 * @param defaultToProjectOnly When true display classes of this project only by default.
220 * When false any class path will be considered. The user can always toggle this.
222 public UiClassAttributeNode(String referenceClass, IPostTypeCreationAction postCreationAction,
223 boolean mandatory, AttributeDescriptor attributeDescriptor, UiElementNode uiParent,
224 boolean defaultToProjectOnly) {
225 super(attributeDescriptor, uiParent);
227 mReferenceClass = referenceClass;
228 mPostCreationAction = postCreationAction;
229 mMandatory = mandatory;
230 mDefaultToProjectOnly = defaultToProjectOnly;
234 * Creates a label widget and an associated text field.
236 * As most other parts of the android manifest editor, this assumes the
237 * parent uses a table layout with 2 columns.
240 public void createUiControl(final Composite parent, IManagedForm managedForm) {
241 setManagedForm(managedForm);
242 FormToolkit toolkit = managedForm.getToolkit();
243 TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
245 StringBuilder label = new StringBuilder();
246 label.append("<form><p><a href='unused'>");
247 label.append(desc.getUiName());
248 label.append("</a></p></form>");
249 FormText formText = SectionHelper.createFormText(parent, toolkit, true /* isHtml */,
250 label.toString(), true /* setupLayoutData */);
251 formText.addHyperlinkListener(new HyperlinkAdapter() {
253 public void linkActivated(HyperlinkEvent e) {
254 super.linkActivated(e);
258 formText.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
259 SectionHelper.addControlTooltip(formText, desc.getTooltip());
261 Composite composite = toolkit.createComposite(parent);
262 composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
263 GridLayout gl = new GridLayout(2, false);
264 gl.marginHeight = gl.marginWidth = 0;
265 composite.setLayout(gl);
266 // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
267 // for the text field below
268 toolkit.paintBordersFor(composite);
270 final Text text = toolkit.createText(composite, getCurrentValue());
271 GridData gd = new GridData(GridData.FILL_HORIZONTAL);
272 gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK
273 text.setLayoutData(gd);
274 Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH);
278 browseButton.addSelectionListener(new SelectionAdapter() {
280 public void widgetSelected(SelectionEvent e) {
281 super.widgetSelected(e);
289 * Add a modify listener that will check the validity of the class
292 protected void onAddValidators(final Text text) {
293 ModifyListener listener = new ModifyListener() {
294 public void modifyText(ModifyEvent e) {
296 String textValue = text.getText().trim();
297 if (textValue.length() == 0) {
299 setErrorMessage("Value is mandatory", text);
301 setErrorMessage(null, text);
305 // first we need the current java package.
306 String javaPackage = getManifestPackage();
308 // build the fully qualified name of the class
309 String className = AndroidManifest.combinePackageAndClassName(
310 javaPackage, textValue);
312 // only test the vilibility for activities.
313 boolean testVisibility = SdkConstants.CLASS_ACTIVITY.equals(
317 setErrorMessage(BaseProjectHelper.testClassForManifest(
318 BaseProjectHelper.getJavaProject(getProject()), className,
319 mReferenceClass, testVisibility), text);
320 } catch (CoreException ce) {
321 setErrorMessage(ce.getMessage(), text);
326 text.addModifyListener(listener);
328 // Make sure the validator removes its message(s) when the widget is disposed
329 text.addDisposeListener(new DisposeListener() {
330 public void widgetDisposed(DisposeEvent e) {
331 // we don't want to use setErrorMessage, because we don't want to reset
332 // the error flag in the UiAttributeNode
333 getManagedForm().getMessageManager().removeMessage(text, text);
337 // Finally call the validator once to make sure the initial value is processed
338 listener.modifyText(null);
341 private void handleBrowseClick() {
342 Text text = getTextWidget();
344 // we need to get the project of the manifest.
345 IProject project = getProject();
346 if (project != null) {
348 // Create a search scope including only the source folder of the current
350 IPackageFragmentRoot[] packageFragmentRoots = getPackageFragmentRoots(project,
351 true /*include_containers*/);
352 IJavaSearchScope scope = SearchEngine.createJavaSearchScope(
353 packageFragmentRoots,
357 SelectionDialog dlg = JavaUI.createTypeDialog(text.getShell(),
358 PlatformUI.getWorkbench().getProgressService(),
360 IJavaElementSearchConstants.CONSIDER_CLASSES, // style
361 false, // no multiple selection
362 "**", //$NON-NLS-1$ //filter
363 new HierarchyTypeSelection(project, mReferenceClass));
364 dlg.setMessage(String.format("Select class name for element %1$s:",
365 getUiParent().getBreadcrumbTrailDescription(false /* include_root */)));
366 if (dlg instanceof ITypeSelectionComponent) {
367 ((ITypeSelectionComponent)dlg).triggerSearch();
370 if (dlg.open() == Window.OK) {
371 Object[] results = dlg.getResult();
372 if (results.length == 1) {
373 handleNewType((IType)results[0]);
376 } catch (JavaModelException e1) {
377 AdtPlugin.log(e1, "UiClassAttributeNode HandleBrowser failed");
382 private void handleLabelClick() {
383 // get the current value
384 String className = getTextWidget().getText().trim();
386 // get the package name from the manifest.
387 String packageName = getManifestPackage();
389 if (className.length() == 0) {
390 createNewClass(packageName, null /* className */);
392 // build back the fully qualified class name.
393 String fullClassName = className;
394 if (className.startsWith(".")) { //$NON-NLS-1$
395 fullClassName = packageName + className;
397 String[] segments = className.split(AndroidConstants.RE_DOT);
398 if (segments.length == 1) {
399 fullClassName = packageName + "." + className; //$NON-NLS-1$
403 // in case the type is enclosed, we need to replace the $ with .
404 fullClassName = fullClassName.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS2$
406 // now we try to find the file that contains this class and we open it in the editor.
407 IProject project = getProject();
408 IJavaProject javaProject = JavaCore.create(project);
411 IType result = javaProject.findType(fullClassName);
412 if (result != null) {
413 JavaUI.openInEditor(result);
415 // split the last segment from the fullClassname
416 int index = fullClassName.lastIndexOf('.');
418 createNewClass(fullClassName.substring(0, index),
419 fullClassName.substring(index+1));
421 createNewClass(packageName, className);
424 } catch (JavaModelException e) {
425 AdtPlugin.log(e, "UiClassAttributeNode HandleLabel failed");
426 } catch (PartInitException e) {
427 AdtPlugin.log(e, "UiClassAttributeNode HandleLabel failed");
432 private IProject getProject() {
433 UiElementNode uiNode = getUiParent();
434 AndroidXmlEditor editor = uiNode.getEditor();
435 IEditorInput input = editor.getEditorInput();
436 if (input instanceof IFileEditorInput) {
437 // from the file editor we can get the IFile object, and from it, the IProject.
438 IFile file = ((IFileEditorInput)input).getFile();
439 return file.getProject();
447 * Returns the current value of the /manifest/package attribute.
448 * @return the package or an empty string if not found
450 private String getManifestPackage() {
451 // get the root uiNode to get the 'package' attribute value.
452 UiElementNode rootNode = getUiParent().getUiRoot();
454 Element xmlElement = (Element) rootNode.getXmlNode();
456 if (xmlElement != null) {
457 return xmlElement.getAttribute(AndroidManifestDescriptors.PACKAGE_ATTR);
459 return ""; //$NON-NLS-1$
464 * Computes and return the {@link IPackageFragmentRoot}s corresponding to the source folders of
465 * the specified project.
466 * @param project the project
467 * @param include_containers True to include containers
468 * @return an array of IPackageFragmentRoot.
470 private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project,
471 boolean include_containers) {
472 ArrayList<IPackageFragmentRoot> result = new ArrayList<IPackageFragmentRoot>();
474 IJavaProject javaProject = JavaCore.create(project);
475 IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots();
476 for (int i = 0; i < roots.length; i++) {
477 IClasspathEntry entry = roots[i].getRawClasspathEntry();
478 if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE ||
479 (include_containers &&
480 entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER)) {
481 result.add(roots[i]);
484 } catch (JavaModelException e) {
487 return result.toArray(new IPackageFragmentRoot[result.size()]);
490 private void handleNewType(IType type) {
491 Text text = getTextWidget();
493 // get the fully qualified name with $ to properly detect the enclosing types.
494 String name = type.getFullyQualifiedName('$');
496 String packageValue = getManifestPackage();
498 // check if the class doesn't start with the package.
499 if (packageValue.length() > 0 && name.startsWith(packageValue)) {
500 // if it does, we remove the package and the first dot.
501 name = name.substring(packageValue.length() + 1);
503 // look for how many segments we have left.
504 // if one, just write it that way.
505 // if more than one, write it with a leading dot.
506 String[] packages = name.split(AndroidConstants.RE_DOT);
507 if (packages.length == 1) {
510 text.setText("." + name); //$NON-NLS-1$
517 private void createNewClass(String packageName, String className) {
518 // create the wizard page for the class creation, and configure it
519 NewClassWizardPage page = new NewClassWizardPage();
521 // set the parent class
522 page.setSuperClass(mReferenceClass, true /* canBeModified */);
524 // get the source folders as java elements.
525 IPackageFragmentRoot[] roots = getPackageFragmentRoots(getProject(),
526 true /*include_containers*/);
528 IPackageFragmentRoot currentRoot = null;
529 IPackageFragment currentFragment = null;
530 int packageMatchCount = -1;
532 for (IPackageFragmentRoot root : roots) {
533 // Get the java element for the package.
534 // This method is said to always return a IPackageFragment even if the
535 // underlying folder doesn't exist...
536 IPackageFragment fragment = root.getPackageFragment(packageName);
537 if (fragment != null && fragment.exists()) {
538 // we have a perfect match! we use it.
540 currentFragment = fragment;
541 packageMatchCount = -1;
544 // we don't have a match. we look for the fragment with the best match
545 // (ie the closest parent package we can find)
547 IJavaElement[] children;
548 children = root.getChildren();
549 for (IJavaElement child : children) {
550 if (child instanceof IPackageFragment) {
551 fragment = (IPackageFragment)child;
552 if (packageName.startsWith(fragment.getElementName())) {
553 // its a match. get the number of segments
554 String[] segments = fragment.getElementName().split("\\."); //$NON-NLS-1$
555 if (segments.length > packageMatchCount) {
556 packageMatchCount = segments.length;
557 currentFragment = fragment;
563 } catch (JavaModelException e) {
564 // Couldn't get the children: we just ignore this package root.
569 ArrayList<IPackageFragment> createdFragments = null;
571 if (currentRoot != null) {
572 // if we have a perfect match, we set it and we're done.
573 if (packageMatchCount == -1) {
574 page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
575 page.setPackageFragment(currentFragment, true /* canBeModified */);
577 // we have a partial match.
578 // create the package. We have to start with the first segment so that we
579 // know what to delete in case of a cancel.
581 createdFragments = new ArrayList<IPackageFragment>();
583 int totalCount = packageName.split("\\.").length; //$NON-NLS-1$
586 // skip the matching packages
587 while (count < packageMatchCount) {
588 index = packageName.indexOf('.', index+1);
592 // create the rest of the segments, except for the last one as indexOf will
594 while (count < totalCount - 1) {
595 index = packageName.indexOf('.', index+1);
597 createdFragments.add(currentRoot.createPackageFragment(
598 packageName.substring(0, index),
599 true /* force*/, new NullProgressMonitor()));
602 // create the last package
603 createdFragments.add(currentRoot.createPackageFragment(
604 packageName, true /* force*/, new NullProgressMonitor()));
606 // set the root and fragment in the Wizard page
607 page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
608 page.setPackageFragment(createdFragments.get(createdFragments.size()-1),
609 true /* canBeModified */);
610 } catch (JavaModelException e) {
611 // if we can't create the packages, there's a problem. we revert to the default
613 for (IPackageFragmentRoot root : roots) {
614 // Get the java element for the package.
615 // This method is said to always return a IPackageFragment even if the
616 // underlying folder doesn't exist...
617 IPackageFragment fragment = root.getPackageFragment(packageName);
618 if (fragment != null && fragment.exists()) {
619 page.setPackageFragmentRoot(root, true /* canBeModified*/);
620 page.setPackageFragment(fragment, true /* canBeModified */);
626 } else if (roots.length > 0) {
627 // if we haven't found a valid fragment, we set the root to the first source folder.
628 page.setPackageFragmentRoot(roots[0], true /* canBeModified*/);
631 // if we have a starting class name we use it
632 if (className != null) {
633 page.setTypeName(className, true /* canBeModified*/);
636 // create the action that will open it the wizard.
637 OpenNewClassWizardAction action = new OpenNewClassWizardAction();
638 action.setConfiguredWizardPage(page);
640 IJavaElement element = action.getCreatedElement();
642 if (element != null) {
643 if (element.getElementType() == IJavaElement.TYPE) {
645 IType type = (IType)element;
647 if (mPostCreationAction != null) {
648 mPostCreationAction.processNewType(type);
654 // lets delete the packages we created just for this.
655 // we need to start with the leaf and go up
656 if (createdFragments != null) {
658 for (int i = createdFragments.size() - 1 ; i >= 0 ; i--) {
659 createdFragments.get(i).delete(true /* force*/, new NullProgressMonitor());
661 } catch (JavaModelException e) {
669 * Sets the error messages. If message is <code>null</code>, the message is removed.
670 * @param message the message to set, or <code>null</code> to remove the current message
671 * @param textWidget the {@link Text} widget associated to the message.
673 private final void setErrorMessage(String message, Text textWidget) {
674 if (message != null) {
676 getManagedForm().getMessageManager().addMessage(textWidget, message, null /* data */,
677 IMessageProvider.ERROR, textWidget);
680 getManagedForm().getMessageManager().removeMessage(textWidget, textWidget);
685 public String[] getPossibleValues(String prefix) {
686 // TODO: compute a list of existing classes for content assist completion