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.AndroidEditor;
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.xml.AndroidManifest;
31 import org.eclipse.core.resources.IFile;
32 import org.eclipse.core.resources.IProject;
33 import org.eclipse.core.runtime.CoreException;
34 import org.eclipse.core.runtime.NullProgressMonitor;
35 import org.eclipse.jdt.core.Flags;
36 import org.eclipse.jdt.core.IClasspathEntry;
37 import org.eclipse.jdt.core.IJavaElement;
38 import org.eclipse.jdt.core.IJavaProject;
39 import org.eclipse.jdt.core.IPackageFragment;
40 import org.eclipse.jdt.core.IPackageFragmentRoot;
41 import org.eclipse.jdt.core.IType;
42 import org.eclipse.jdt.core.ITypeHierarchy;
43 import org.eclipse.jdt.core.JavaCore;
44 import org.eclipse.jdt.core.JavaModelException;
45 import org.eclipse.jdt.core.search.IJavaSearchScope;
46 import org.eclipse.jdt.core.search.SearchEngine;
47 import org.eclipse.jdt.ui.IJavaElementSearchConstants;
48 import org.eclipse.jdt.ui.JavaUI;
49 import org.eclipse.jdt.ui.actions.OpenNewClassWizardAction;
50 import org.eclipse.jdt.ui.dialogs.ITypeInfoFilterExtension;
51 import org.eclipse.jdt.ui.dialogs.ITypeInfoRequestor;
52 import org.eclipse.jdt.ui.dialogs.ITypeSelectionComponent;
53 import org.eclipse.jdt.ui.dialogs.TypeSelectionExtension;
54 import org.eclipse.jdt.ui.wizards.NewClassWizardPage;
55 import org.eclipse.jface.dialogs.IMessageProvider;
56 import org.eclipse.jface.window.Window;
57 import org.eclipse.swt.SWT;
58 import org.eclipse.swt.events.DisposeEvent;
59 import org.eclipse.swt.events.DisposeListener;
60 import org.eclipse.swt.events.ModifyEvent;
61 import org.eclipse.swt.events.ModifyListener;
62 import org.eclipse.swt.events.SelectionAdapter;
63 import org.eclipse.swt.events.SelectionEvent;
64 import org.eclipse.swt.layout.GridData;
65 import org.eclipse.swt.layout.GridLayout;
66 import org.eclipse.swt.widgets.Button;
67 import org.eclipse.swt.widgets.Composite;
68 import org.eclipse.swt.widgets.Control;
69 import org.eclipse.swt.widgets.Text;
70 import org.eclipse.ui.IEditorInput;
71 import org.eclipse.ui.IFileEditorInput;
72 import org.eclipse.ui.PartInitException;
73 import org.eclipse.ui.PlatformUI;
74 import org.eclipse.ui.dialogs.SelectionDialog;
75 import org.eclipse.ui.forms.IManagedForm;
76 import org.eclipse.ui.forms.events.HyperlinkAdapter;
77 import org.eclipse.ui.forms.events.HyperlinkEvent;
78 import org.eclipse.ui.forms.widgets.FormText;
79 import org.eclipse.ui.forms.widgets.FormToolkit;
80 import org.eclipse.ui.forms.widgets.TableWrapData;
81 import org.w3c.dom.Element;
83 import java.util.ArrayList;
86 * Represents an XML attribute for a class, that can be modified using a simple text field or
87 * a dialog to choose an existing class. Also, there's a link to create a new class.
89 * See {@link UiTextAttributeNode} for more information.
91 public class UiClassAttributeNode extends UiTextAttributeNode {
93 private String mReferenceClass;
94 private IPostTypeCreationAction mPostCreationAction;
95 private boolean mMandatory;
96 private final boolean mDefaultToProjectOnly;
98 private class HierarchyTypeSelection extends TypeSelectionExtension {
100 private IJavaProject mJavaProject;
101 private IType mReferenceType;
102 private Button mProjectOnly;
103 private boolean mUseProjectOnly;
105 public HierarchyTypeSelection(IProject project, String referenceClass)
106 throws JavaModelException {
107 mJavaProject = JavaCore.create(project);
108 mReferenceType = mJavaProject.findType(referenceClass);
112 public ITypeInfoFilterExtension getFilterExtension() {
113 return new ITypeInfoFilterExtension() {
114 public boolean select(ITypeInfoRequestor typeInfoRequestor) {
116 boolean projectOnly = mUseProjectOnly;
118 String packageName = typeInfoRequestor.getPackageName();
119 String typeName = typeInfoRequestor.getTypeName();
120 String enclosingType = typeInfoRequestor.getEnclosingName();
122 // build the full class name.
123 StringBuilder sb = new StringBuilder(packageName);
125 if (enclosingType.length() > 0) {
126 sb.append(enclosingType);
131 String className = sb.toString();
134 IType type = mJavaProject.findType(className);
140 // don't display abstract classes
141 if ((type.getFlags() & Flags.AccAbstract) != 0) {
145 // if project-only is selected, make sure the package fragment is
146 // an actual source (thus "from this project").
148 IPackageFragment frag = type.getPackageFragment();
149 if (frag == null || frag.getKind() != IPackageFragmentRoot.K_SOURCE) {
154 // get the type hierarchy and reference type is one of the super classes.
155 ITypeHierarchy hierarchy = type.newSupertypeHierarchy(
156 new NullProgressMonitor());
158 IType[] supertypes = hierarchy.getAllSupertypes(type);
159 int n = supertypes.length;
160 for (int i = 0; i < n; i++) {
161 IType st = supertypes[i];
162 if (mReferenceType.equals(st)) {
166 } catch (JavaModelException e) {
175 public Control createContentArea(Composite parent) {
177 mProjectOnly = new Button(parent, SWT.CHECK);
178 mProjectOnly.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
179 mProjectOnly.setText(String.format("Display classes from sources of project '%s' only",
180 mJavaProject.getProject().getName()));
182 mUseProjectOnly = mDefaultToProjectOnly;
183 mProjectOnly.setSelection(mUseProjectOnly);
185 mProjectOnly.addSelectionListener(new SelectionAdapter() {
187 public void widgetSelected(SelectionEvent e) {
188 super.widgetSelected(e);
189 mUseProjectOnly = mProjectOnly.getSelection();
190 getTypeSelectionComponent().triggerSearch();
194 return super.createContentArea(parent);
199 * Classes which implement this interface provide a method processing newly created classes.
201 public static interface IPostTypeCreationAction {
203 * Sent to process a newly created class.
204 * @param newType the IType representing the newly created class.
206 public void processNewType(IType newType);
210 * Creates a {@link UiClassAttributeNode} object that will display ui to select or create
212 * @param referenceClass The allowed supertype of the classes that are to be selected
213 * or created. Can be null.
214 * @param postCreationAction a {@link IPostTypeCreationAction} object handling post creation
215 * modification of the class.
216 * @param mandatory indicates if the class value is mandatory
217 * @param attributeDescriptor the {@link AttributeDescriptor} object linked to the Ui Node.
218 * @param defaultToProjectOnly When true display classes of this project only by default.
219 * When false any class path will be considered. The user can always toggle this.
221 public UiClassAttributeNode(String referenceClass, IPostTypeCreationAction postCreationAction,
222 boolean mandatory, AttributeDescriptor attributeDescriptor, UiElementNode uiParent,
223 boolean defaultToProjectOnly) {
224 super(attributeDescriptor, uiParent);
226 mReferenceClass = referenceClass;
227 mPostCreationAction = postCreationAction;
228 mMandatory = mandatory;
229 mDefaultToProjectOnly = defaultToProjectOnly;
233 * Creates a label widget and an associated text field.
235 * As most other parts of the android manifest editor, this assumes the
236 * parent uses a table layout with 2 columns.
239 public void createUiControl(final Composite parent, IManagedForm managedForm) {
240 setManagedForm(managedForm);
241 FormToolkit toolkit = managedForm.getToolkit();
242 TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
244 StringBuilder label = new StringBuilder();
245 label.append("<form><p><a href='unused'>");
246 label.append(desc.getUiName());
247 label.append("</a></p></form>");
248 FormText formText = SectionHelper.createFormText(parent, toolkit, true /* isHtml */,
249 label.toString(), true /* setupLayoutData */);
250 formText.addHyperlinkListener(new HyperlinkAdapter() {
252 public void linkActivated(HyperlinkEvent e) {
253 super.linkActivated(e);
257 formText.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
258 SectionHelper.addControlTooltip(formText, desc.getTooltip());
260 Composite composite = toolkit.createComposite(parent);
261 composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
262 GridLayout gl = new GridLayout(2, false);
263 gl.marginHeight = gl.marginWidth = 0;
264 composite.setLayout(gl);
265 // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
266 // for the text field below
267 toolkit.paintBordersFor(composite);
269 final Text text = toolkit.createText(composite, getCurrentValue());
270 GridData gd = new GridData(GridData.FILL_HORIZONTAL);
271 gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK
272 text.setLayoutData(gd);
273 Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH);
277 browseButton.addSelectionListener(new SelectionAdapter() {
279 public void widgetSelected(SelectionEvent e) {
280 super.widgetSelected(e);
288 * Add a modify listener that will check the validity of the class
291 protected void onAddValidators(final Text text) {
292 ModifyListener listener = new ModifyListener() {
293 public void modifyText(ModifyEvent e) {
295 String textValue = text.getText().trim();
296 if (textValue.length() == 0) {
298 setErrorMessage("Value is mandatory", text);
300 setErrorMessage(null, text);
304 // first we need the current java package.
305 String javaPackage = getManifestPackage();
307 // build the fully qualified name of the class
308 String className = AndroidManifest.combinePackageAndClassName(
309 javaPackage, textValue);
311 // only test the vilibility for activities.
312 boolean testVisibility = AndroidConstants.CLASS_ACTIVITY.equals(
316 setErrorMessage(BaseProjectHelper.testClassForManifest(
317 BaseProjectHelper.getJavaProject(getProject()), className,
318 mReferenceClass, testVisibility), text);
319 } catch (CoreException ce) {
320 setErrorMessage(ce.getMessage(), text);
325 text.addModifyListener(listener);
327 // Make sure the validator removes its message(s) when the widget is disposed
328 text.addDisposeListener(new DisposeListener() {
329 public void widgetDisposed(DisposeEvent e) {
330 // we don't want to use setErrorMessage, because we don't want to reset
331 // the error flag in the UiAttributeNode
332 getManagedForm().getMessageManager().removeMessage(text, text);
336 // Finally call the validator once to make sure the initial value is processed
337 listener.modifyText(null);
340 private void handleBrowseClick() {
341 Text text = getTextWidget();
343 // we need to get the project of the manifest.
344 IProject project = getProject();
345 if (project != null) {
347 // Create a search scope including only the source folder of the current
349 IPackageFragmentRoot[] packageFragmentRoots = getPackageFragmentRoots(project,
350 true /*include_containers*/);
351 IJavaSearchScope scope = SearchEngine.createJavaSearchScope(
352 packageFragmentRoots,
356 SelectionDialog dlg = JavaUI.createTypeDialog(text.getShell(),
357 PlatformUI.getWorkbench().getProgressService(),
359 IJavaElementSearchConstants.CONSIDER_CLASSES, // style
360 false, // no multiple selection
361 "**", //$NON-NLS-1$ //filter
362 new HierarchyTypeSelection(project, mReferenceClass));
363 dlg.setMessage(String.format("Select class name for element %1$s:",
364 getUiParent().getBreadcrumbTrailDescription(false /* include_root */)));
365 if (dlg instanceof ITypeSelectionComponent) {
366 ((ITypeSelectionComponent)dlg).triggerSearch();
369 if (dlg.open() == Window.OK) {
370 Object[] results = dlg.getResult();
371 if (results.length == 1) {
372 handleNewType((IType)results[0]);
375 } catch (JavaModelException e1) {
376 AdtPlugin.log(e1, "UiClassAttributeNode HandleBrowser failed");
381 private void handleLabelClick() {
382 // get the current value
383 String className = getTextWidget().getText().trim();
385 // get the package name from the manifest.
386 String packageName = getManifestPackage();
388 if (className.length() == 0) {
389 createNewClass(packageName, null /* className */);
391 // build back the fully qualified class name.
392 String fullClassName = className;
393 if (className.startsWith(".")) { //$NON-NLS-1$
394 fullClassName = packageName + className;
396 String[] segments = className.split(AndroidConstants.RE_DOT);
397 if (segments.length == 1) {
398 fullClassName = packageName + "." + className; //$NON-NLS-1$
402 // in case the type is enclosed, we need to replace the $ with .
403 fullClassName = fullClassName.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS2$
405 // now we try to find the file that contains this class and we open it in the editor.
406 IProject project = getProject();
407 IJavaProject javaProject = JavaCore.create(project);
410 IType result = javaProject.findType(fullClassName);
411 if (result != null) {
412 JavaUI.openInEditor(result);
414 // split the last segment from the fullClassname
415 int index = fullClassName.lastIndexOf('.');
417 createNewClass(fullClassName.substring(0, index),
418 fullClassName.substring(index+1));
420 createNewClass(packageName, className);
423 } catch (JavaModelException e) {
424 AdtPlugin.log(e, "UiClassAttributeNode HandleLabel failed");
425 } catch (PartInitException e) {
426 AdtPlugin.log(e, "UiClassAttributeNode HandleLabel failed");
431 private IProject getProject() {
432 UiElementNode uiNode = getUiParent();
433 AndroidEditor editor = uiNode.getEditor();
434 IEditorInput input = editor.getEditorInput();
435 if (input instanceof IFileEditorInput) {
436 // from the file editor we can get the IFile object, and from it, the IProject.
437 IFile file = ((IFileEditorInput)input).getFile();
438 return file.getProject();
446 * Returns the current value of the /manifest/package attribute.
447 * @return the package or an empty string if not found
449 private String getManifestPackage() {
450 // get the root uiNode to get the 'package' attribute value.
451 UiElementNode rootNode = getUiParent().getUiRoot();
453 Element xmlElement = (Element) rootNode.getXmlNode();
455 if (xmlElement != null) {
456 return xmlElement.getAttribute(AndroidManifestDescriptors.PACKAGE_ATTR);
458 return ""; //$NON-NLS-1$
463 * Computes and return the {@link IPackageFragmentRoot}s corresponding to the source folders of
464 * the specified project.
465 * @param project the project
466 * @param include_containers True to include containers
467 * @return an array of IPackageFragmentRoot.
469 private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project,
470 boolean include_containers) {
471 ArrayList<IPackageFragmentRoot> result = new ArrayList<IPackageFragmentRoot>();
473 IJavaProject javaProject = JavaCore.create(project);
474 IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots();
475 for (int i = 0; i < roots.length; i++) {
476 IClasspathEntry entry = roots[i].getRawClasspathEntry();
477 if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE ||
478 (include_containers &&
479 entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER)) {
480 result.add(roots[i]);
483 } catch (JavaModelException e) {
486 return result.toArray(new IPackageFragmentRoot[result.size()]);
489 private void handleNewType(IType type) {
490 Text text = getTextWidget();
492 // get the fully qualified name with $ to properly detect the enclosing types.
493 String name = type.getFullyQualifiedName('$');
495 String packageValue = getManifestPackage();
497 // check if the class doesn't start with the package.
498 if (packageValue.length() > 0 && name.startsWith(packageValue)) {
499 // if it does, we remove the package and the first dot.
500 name = name.substring(packageValue.length() + 1);
502 // look for how many segments we have left.
503 // if one, just write it that way.
504 // if more than one, write it with a leading dot.
505 String[] packages = name.split(AndroidConstants.RE_DOT);
506 if (packages.length == 1) {
509 text.setText("." + name); //$NON-NLS-1$
516 private void createNewClass(String packageName, String className) {
517 // create the wizard page for the class creation, and configure it
518 NewClassWizardPage page = new NewClassWizardPage();
520 // set the parent class
521 page.setSuperClass(mReferenceClass, true /* canBeModified */);
523 // get the source folders as java elements.
524 IPackageFragmentRoot[] roots = getPackageFragmentRoots(getProject(),
525 true /*include_containers*/);
527 IPackageFragmentRoot currentRoot = null;
528 IPackageFragment currentFragment = null;
529 int packageMatchCount = -1;
531 for (IPackageFragmentRoot root : roots) {
532 // Get the java element for the package.
533 // This method is said to always return a IPackageFragment even if the
534 // underlying folder doesn't exist...
535 IPackageFragment fragment = root.getPackageFragment(packageName);
536 if (fragment != null && fragment.exists()) {
537 // we have a perfect match! we use it.
539 currentFragment = fragment;
540 packageMatchCount = -1;
543 // we don't have a match. we look for the fragment with the best match
544 // (ie the closest parent package we can find)
546 IJavaElement[] children;
547 children = root.getChildren();
548 for (IJavaElement child : children) {
549 if (child instanceof IPackageFragment) {
550 fragment = (IPackageFragment)child;
551 if (packageName.startsWith(fragment.getElementName())) {
552 // its a match. get the number of segments
553 String[] segments = fragment.getElementName().split("\\."); //$NON-NLS-1$
554 if (segments.length > packageMatchCount) {
555 packageMatchCount = segments.length;
556 currentFragment = fragment;
562 } catch (JavaModelException e) {
563 // Couldn't get the children: we just ignore this package root.
568 ArrayList<IPackageFragment> createdFragments = null;
570 if (currentRoot != null) {
571 // if we have a perfect match, we set it and we're done.
572 if (packageMatchCount == -1) {
573 page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
574 page.setPackageFragment(currentFragment, true /* canBeModified */);
576 // we have a partial match.
577 // create the package. We have to start with the first segment so that we
578 // know what to delete in case of a cancel.
580 createdFragments = new ArrayList<IPackageFragment>();
582 int totalCount = packageName.split("\\.").length; //$NON-NLS-1$
585 // skip the matching packages
586 while (count < packageMatchCount) {
587 index = packageName.indexOf('.', index+1);
591 // create the rest of the segments, except for the last one as indexOf will
593 while (count < totalCount - 1) {
594 index = packageName.indexOf('.', index+1);
596 createdFragments.add(currentRoot.createPackageFragment(
597 packageName.substring(0, index),
598 true /* force*/, new NullProgressMonitor()));
601 // create the last package
602 createdFragments.add(currentRoot.createPackageFragment(
603 packageName, true /* force*/, new NullProgressMonitor()));
605 // set the root and fragment in the Wizard page
606 page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
607 page.setPackageFragment(createdFragments.get(createdFragments.size()-1),
608 true /* canBeModified */);
609 } catch (JavaModelException e) {
610 // if we can't create the packages, there's a problem. we revert to the default
612 for (IPackageFragmentRoot root : roots) {
613 // Get the java element for the package.
614 // This method is said to always return a IPackageFragment even if the
615 // underlying folder doesn't exist...
616 IPackageFragment fragment = root.getPackageFragment(packageName);
617 if (fragment != null && fragment.exists()) {
618 page.setPackageFragmentRoot(root, true /* canBeModified*/);
619 page.setPackageFragment(fragment, true /* canBeModified */);
625 } else if (roots.length > 0) {
626 // if we haven't found a valid fragment, we set the root to the first source folder.
627 page.setPackageFragmentRoot(roots[0], true /* canBeModified*/);
630 // if we have a starting class name we use it
631 if (className != null) {
632 page.setTypeName(className, true /* canBeModified*/);
635 // create the action that will open it the wizard.
636 OpenNewClassWizardAction action = new OpenNewClassWizardAction();
637 action.setConfiguredWizardPage(page);
639 IJavaElement element = action.getCreatedElement();
641 if (element != null) {
642 if (element.getElementType() == IJavaElement.TYPE) {
644 IType type = (IType)element;
646 if (mPostCreationAction != null) {
647 mPostCreationAction.processNewType(type);
653 // lets delete the packages we created just for this.
654 // we need to start with the leaf and go up
655 if (createdFragments != null) {
657 for (int i = createdFragments.size() - 1 ; i >= 0 ; i--) {
658 createdFragments.get(i).delete(true /* force*/, new NullProgressMonitor());
660 } catch (JavaModelException e) {
668 * Sets the error messages. If message is <code>null</code>, the message is removed.
669 * @param message the message to set, or <code>null</code> to remove the current message
670 * @param textWidget the {@link Text} widget associated to the message.
672 private final void setErrorMessage(String message, Text textWidget) {
673 if (message != null) {
675 getManagedForm().getMessageManager().addMessage(textWidget, message, null /* data */,
676 IMessageProvider.ERROR, textWidget);
679 getManagedForm().getMessageManager().removeMessage(textWidget, textWidget);
684 public String[] getPossibleValues(String prefix) {
685 // TODO: compute a list of existing classes for content assist completion