blob: 4c829d9eca2086d434f4676a4d5253cfa2f39d05 [file] [log] [blame]
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.eclipse.org/org/documents/epl-v10.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ide.eclipse.adt.internal.editors.manifest.model;
import com.android.SdkConstants;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiTextAttributeNode;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.xml.AndroidManifest;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.ui.IJavaElementSearchConstants;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jdt.ui.actions.OpenNewClassWizardAction;
import org.eclipse.jdt.ui.dialogs.ITypeInfoFilterExtension;
import org.eclipse.jdt.ui.dialogs.ITypeInfoRequestor;
import org.eclipse.jdt.ui.dialogs.ITypeSelectionComponent;
import org.eclipse.jdt.ui.dialogs.TypeSelectionExtension;
import org.eclipse.jdt.ui.wizards.NewClassWizardPage;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.SelectionDialog;
import org.eclipse.ui.forms.IManagedForm;
import org.eclipse.ui.forms.events.HyperlinkAdapter;
import org.eclipse.ui.forms.events.HyperlinkEvent;
import org.eclipse.ui.forms.widgets.FormText;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.forms.widgets.TableWrapData;
import org.w3c.dom.Element;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Represents an XML attribute for a class, that can be modified using a simple text field or
* a dialog to choose an existing class. Also, there's a link to create a new class.
* <p/>
* See {@link UiTextAttributeNode} for more information.
*/
public class UiClassAttributeNode extends UiTextAttributeNode {
private String mReferenceClass;
private IPostTypeCreationAction mPostCreationAction;
private boolean mMandatory;
private final boolean mDefaultToProjectOnly;
private class HierarchyTypeSelection extends TypeSelectionExtension {
private IJavaProject mJavaProject;
private IType mReferenceType;
private Button mProjectOnly;
private boolean mUseProjectOnly;
public HierarchyTypeSelection(IProject project, String referenceClass)
throws JavaModelException {
mJavaProject = JavaCore.create(project);
mReferenceType = mJavaProject.findType(referenceClass);
}
@Override
public ITypeInfoFilterExtension getFilterExtension() {
return new ITypeInfoFilterExtension() {
@Override
public boolean select(ITypeInfoRequestor typeInfoRequestor) {
boolean projectOnly = mUseProjectOnly;
String packageName = typeInfoRequestor.getPackageName();
String typeName = typeInfoRequestor.getTypeName();
String enclosingType = typeInfoRequestor.getEnclosingName();
// build the full class name.
StringBuilder sb = new StringBuilder(packageName);
sb.append('.');
if (enclosingType.length() > 0) {
sb.append(enclosingType);
sb.append('.');
}
sb.append(typeName);
String className = sb.toString();
try {
IType type = mJavaProject.findType(className);
if (type == null) {
return false;
}
// don't display abstract classes
if ((type.getFlags() & Flags.AccAbstract) != 0) {
return false;
}
// if project-only is selected, make sure the package fragment is
// an actual source (thus "from this project").
if (projectOnly) {
IPackageFragment frag = type.getPackageFragment();
if (frag == null || frag.getKind() != IPackageFragmentRoot.K_SOURCE) {
return false;
}
}
// get the type hierarchy and reference type is one of the super classes.
ITypeHierarchy hierarchy = type.newSupertypeHierarchy(
new NullProgressMonitor());
IType[] supertypes = hierarchy.getAllSupertypes(type);
int n = supertypes.length;
for (int i = 0; i < n; i++) {
IType st = supertypes[i];
if (mReferenceType.equals(st)) {
return true;
}
}
} catch (JavaModelException e) {
}
return false;
}
};
}
@Override
public Control createContentArea(Composite parent) {
mProjectOnly = new Button(parent, SWT.CHECK);
mProjectOnly.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
mProjectOnly.setText(String.format("Display classes from sources of project '%s' only",
mJavaProject.getProject().getName()));
mUseProjectOnly = mDefaultToProjectOnly;
mProjectOnly.setSelection(mUseProjectOnly);
mProjectOnly.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
super.widgetSelected(e);
mUseProjectOnly = mProjectOnly.getSelection();
getTypeSelectionComponent().triggerSearch();
}
});
return super.createContentArea(parent);
}
}
/**
* Classes which implement this interface provide a method processing newly created classes.
*/
public static interface IPostTypeCreationAction {
/**
* Sent to process a newly created class.
* @param newType the IType representing the newly created class.
*/
public void processNewType(IType newType);
}
/**
* Creates a {@link UiClassAttributeNode} object that will display ui to select or create
* classes.
* @param referenceClass The allowed supertype of the classes that are to be selected
* or created. Can be null.
* @param postCreationAction a {@link IPostTypeCreationAction} object handling post creation
* modification of the class.
* @param mandatory indicates if the class value is mandatory
* @param attributeDescriptor the {@link AttributeDescriptor} object linked to the Ui Node.
* @param defaultToProjectOnly When true display classes of this project only by default.
* When false any class path will be considered. The user can always toggle this.
*/
public UiClassAttributeNode(String referenceClass, IPostTypeCreationAction postCreationAction,
boolean mandatory, AttributeDescriptor attributeDescriptor, UiElementNode uiParent,
boolean defaultToProjectOnly) {
super(attributeDescriptor, uiParent);
mReferenceClass = referenceClass;
mPostCreationAction = postCreationAction;
mMandatory = mandatory;
mDefaultToProjectOnly = defaultToProjectOnly;
}
/* (non-java doc)
* Creates a label widget and an associated text field.
* <p/>
* As most other parts of the android manifest editor, this assumes the
* parent uses a table layout with 2 columns.
*/
@Override
public void createUiControl(final Composite parent, IManagedForm managedForm) {
setManagedForm(managedForm);
FormToolkit toolkit = managedForm.getToolkit();
TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
StringBuilder label = new StringBuilder();
label.append("<form><p><a href='unused'>");
label.append(desc.getUiName());
label.append("</a></p></form>");
FormText formText = SectionHelper.createFormText(parent, toolkit, true /* isHtml */,
label.toString(), true /* setupLayoutData */);
formText.addHyperlinkListener(new HyperlinkAdapter() {
@Override
public void linkActivated(HyperlinkEvent e) {
super.linkActivated(e);
handleLabelClick();
}
});
formText.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
SectionHelper.addControlTooltip(formText, desc.getTooltip());
Composite composite = toolkit.createComposite(parent);
composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
GridLayout gl = new GridLayout(2, false);
gl.marginHeight = gl.marginWidth = 0;
composite.setLayout(gl);
// Fixes missing text borders under GTK... also requires adding a 1-pixel margin
// for the text field below
toolkit.paintBordersFor(composite);
final Text text = toolkit.createText(composite, getCurrentValue());
GridData gd = new GridData(GridData.FILL_HORIZONTAL);
gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK
text.setLayoutData(gd);
Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH);
setTextWidget(text);
browseButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
super.widgetSelected(e);
handleBrowseClick();
}
});
}
/* (non-java doc)
*
* Add a modify listener that will check the validity of the class
*/
@Override
protected void onAddValidators(final Text text) {
ModifyListener listener = new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
try {
String textValue = text.getText().trim();
if (textValue.length() == 0) {
if (mMandatory) {
setErrorMessage("Value is mandatory", text);
} else {
setErrorMessage(null, text);
}
return;
}
// first we need the current java package.
String javaPackage = getManifestPackage();
// build the fully qualified name of the class
String className = AndroidManifest.combinePackageAndClassName(
javaPackage, textValue);
// only test the vilibility for activities.
boolean testVisibility = SdkConstants.CLASS_ACTIVITY.equals(
mReferenceClass);
// test the class
setErrorMessage(BaseProjectHelper.testClassForManifest(
BaseProjectHelper.getJavaProject(getProject()), className,
mReferenceClass, testVisibility), text);
} catch (CoreException ce) {
setErrorMessage(ce.getMessage(), text);
}
}
};
text.addModifyListener(listener);
// Make sure the validator removes its message(s) when the widget is disposed
text.addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(DisposeEvent e) {
// we don't want to use setErrorMessage, because we don't want to reset
// the error flag in the UiAttributeNode
getManagedForm().getMessageManager().removeMessage(text, text);
}
});
// Finally call the validator once to make sure the initial value is processed
listener.modifyText(null);
}
private void handleBrowseClick() {
Text text = getTextWidget();
// we need to get the project of the manifest.
IProject project = getProject();
if (project != null) {
// Create a search scope including only the source folder of the current
// project.
IPackageFragmentRoot[] packageFragmentRoots = getPackageFragmentRoots(project,
true /*include_containers*/);
IJavaSearchScope scope = SearchEngine.createJavaSearchScope(
packageFragmentRoots,
false);
try {
SelectionDialog dlg = JavaUI.createTypeDialog(text.getShell(),
PlatformUI.getWorkbench().getProgressService(),
scope,
IJavaElementSearchConstants.CONSIDER_CLASSES, // style
false, // no multiple selection
"**", //$NON-NLS-1$ //filter
new HierarchyTypeSelection(project, mReferenceClass));
dlg.setMessage(String.format("Select class name for element %1$s:",
getUiParent().getBreadcrumbTrailDescription(false /* include_root */)));
if (dlg instanceof ITypeSelectionComponent) {
((ITypeSelectionComponent)dlg).triggerSearch();
}
if (dlg.open() == Window.OK) {
Object[] results = dlg.getResult();
if (results.length == 1) {
handleNewType((IType)results[0]);
}
}
} catch (JavaModelException e1) {
AdtPlugin.log(e1, "UiClassAttributeNode HandleBrowser failed");
}
}
}
private void handleLabelClick() {
// get the current value
String className = getTextWidget().getText().trim();
// get the package name from the manifest.
String packageName = getManifestPackage();
if (className.length() == 0) {
createNewClass(packageName, null /* className */);
} else {
// build back the fully qualified class name.
String fullClassName = className;
if (className.startsWith(".")) { //$NON-NLS-1$
fullClassName = packageName + className;
} else {
String[] segments = className.split(AdtConstants.RE_DOT);
if (segments.length == 1) {
fullClassName = packageName + "." + className; //$NON-NLS-1$
}
}
// in case the type is enclosed, we need to replace the $ with .
fullClassName = fullClassName.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS2$
// now we try to find the file that contains this class and we open it in the editor.
IProject project = getProject();
IJavaProject javaProject = JavaCore.create(project);
try {
IType result = javaProject.findType(fullClassName);
if (result != null) {
JavaUI.openInEditor(result);
} else {
// split the last segment from the fullClassname
int index = fullClassName.lastIndexOf('.');
if (index != -1) {
createNewClass(fullClassName.substring(0, index),
fullClassName.substring(index+1));
} else {
createNewClass(packageName, className);
}
}
} catch (JavaModelException e) {
AdtPlugin.log(e, "UiClassAttributeNode HandleLabel failed");
} catch (PartInitException e) {
AdtPlugin.log(e, "UiClassAttributeNode HandleLabel failed");
}
}
}
private IProject getProject() {
UiElementNode uiNode = getUiParent();
AndroidXmlEditor editor = uiNode.getEditor();
IEditorInput input = editor.getEditorInput();
if (input instanceof IFileEditorInput) {
// from the file editor we can get the IFile object, and from it, the IProject.
IFile file = ((IFileEditorInput)input).getFile();
return file.getProject();
}
return null;
}
/**
* Returns the current value of the /manifest/package attribute.
* @return the package or an empty string if not found
*/
private String getManifestPackage() {
// get the root uiNode to get the 'package' attribute value.
UiElementNode rootNode = getUiParent().getUiRoot();
Element xmlElement = (Element) rootNode.getXmlNode();
if (xmlElement != null) {
return xmlElement.getAttribute(AndroidManifestDescriptors.PACKAGE_ATTR);
}
return ""; //$NON-NLS-1$
}
/**
* Computes and return the {@link IPackageFragmentRoot}s corresponding to the source folders of
* the specified project.
* @param project the project
* @param include_containers True to include containers
* @return an array of IPackageFragmentRoot.
*/
private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project,
boolean include_containers) {
ArrayList<IPackageFragmentRoot> result = new ArrayList<IPackageFragmentRoot>();
try {
IJavaProject javaProject = JavaCore.create(project);
IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots();
for (int i = 0; i < roots.length; i++) {
IClasspathEntry entry = roots[i].getRawClasspathEntry();
if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE ||
(include_containers &&
entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER)) {
result.add(roots[i]);
}
}
} catch (JavaModelException e) {
}
return result.toArray(new IPackageFragmentRoot[result.size()]);
}
private void handleNewType(IType type) {
Text text = getTextWidget();
// get the fully qualified name with $ to properly detect the enclosing types.
String name = type.getFullyQualifiedName('$');
String packageValue = getManifestPackage();
// check if the class doesn't start with the package.
if (packageValue.length() > 0 && name.startsWith(packageValue)) {
// if it does, we remove the package and the first dot.
name = name.substring(packageValue.length() + 1);
// look for how many segments we have left.
// if one, just write it that way.
// if more than one, write it with a leading dot.
String[] packages = name.split(AdtConstants.RE_DOT);
if (packages.length == 1) {
text.setText(name);
} else {
text.setText("." + name); //$NON-NLS-1$
}
} else {
text.setText(name);
}
}
private void createNewClass(String packageName, String className) {
// create the wizard page for the class creation, and configure it
NewClassWizardPage page = new NewClassWizardPage();
// set the parent class
page.setSuperClass(mReferenceClass, true /* canBeModified */);
// get the source folders as java elements.
IPackageFragmentRoot[] roots = getPackageFragmentRoots(getProject(),
true /*include_containers*/);
IPackageFragmentRoot currentRoot = null;
IPackageFragment currentFragment = null;
int packageMatchCount = -1;
for (IPackageFragmentRoot root : roots) {
// Get the java element for the package.
// This method is said to always return a IPackageFragment even if the
// underlying folder doesn't exist...
IPackageFragment fragment = root.getPackageFragment(packageName);
if (fragment != null && fragment.exists()) {
// we have a perfect match! we use it.
currentRoot = root;
currentFragment = fragment;
packageMatchCount = -1;
break;
} else {
// we don't have a match. we look for the fragment with the best match
// (ie the closest parent package we can find)
try {
IJavaElement[] children;
children = root.getChildren();
for (IJavaElement child : children) {
if (child instanceof IPackageFragment) {
fragment = (IPackageFragment)child;
if (packageName.startsWith(fragment.getElementName())) {
// its a match. get the number of segments
String[] segments = fragment.getElementName().split("\\."); //$NON-NLS-1$
if (segments.length > packageMatchCount) {
packageMatchCount = segments.length;
currentFragment = fragment;
currentRoot = root;
}
}
}
}
} catch (JavaModelException e) {
// Couldn't get the children: we just ignore this package root.
}
}
}
ArrayList<IPackageFragment> createdFragments = null;
if (currentRoot != null) {
// if we have a perfect match, we set it and we're done.
if (packageMatchCount == -1) {
page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
page.setPackageFragment(currentFragment, true /* canBeModified */);
} else {
// we have a partial match.
// create the package. We have to start with the first segment so that we
// know what to delete in case of a cancel.
try {
createdFragments = new ArrayList<IPackageFragment>();
int totalCount = packageName.split("\\.").length; //$NON-NLS-1$
int count = 0;
int index = -1;
// skip the matching packages
while (count < packageMatchCount) {
index = packageName.indexOf('.', index+1);
count++;
}
// create the rest of the segments, except for the last one as indexOf will
// return -1;
while (count < totalCount - 1) {
index = packageName.indexOf('.', index+1);
count++;
createdFragments.add(currentRoot.createPackageFragment(
packageName.substring(0, index),
true /* force*/, new NullProgressMonitor()));
}
// create the last package
createdFragments.add(currentRoot.createPackageFragment(
packageName, true /* force*/, new NullProgressMonitor()));
// set the root and fragment in the Wizard page
page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
page.setPackageFragment(createdFragments.get(createdFragments.size()-1),
true /* canBeModified */);
} catch (JavaModelException e) {
// if we can't create the packages, there's a problem. we revert to the default
// package
for (IPackageFragmentRoot root : roots) {
// Get the java element for the package.
// This method is said to always return a IPackageFragment even if the
// underlying folder doesn't exist...
IPackageFragment fragment = root.getPackageFragment(packageName);
if (fragment != null && fragment.exists()) {
page.setPackageFragmentRoot(root, true /* canBeModified*/);
page.setPackageFragment(fragment, true /* canBeModified */);
break;
}
}
}
}
} else if (roots.length > 0) {
// if we haven't found a valid fragment, we set the root to the first source folder.
page.setPackageFragmentRoot(roots[0], true /* canBeModified*/);
}
// if we have a starting class name we use it
if (className != null) {
page.setTypeName(className, true /* canBeModified*/);
}
// create the action that will open it the wizard.
OpenNewClassWizardAction action = new OpenNewClassWizardAction();
action.setConfiguredWizardPage(page);
action.run();
IJavaElement element = action.getCreatedElement();
if (element != null) {
if (element.getElementType() == IJavaElement.TYPE) {
IType type = (IType)element;
if (mPostCreationAction != null) {
mPostCreationAction.processNewType(type);
}
handleNewType(type);
}
} else {
// lets delete the packages we created just for this.
// we need to start with the leaf and go up
if (createdFragments != null) {
try {
for (int i = createdFragments.size() - 1 ; i >= 0 ; i--) {
createdFragments.get(i).delete(true /* force*/, new NullProgressMonitor());
}
} catch (JavaModelException e) {
e.printStackTrace();
}
}
}
}
/**
* Sets the error messages. If message is <code>null</code>, the message is removed.
* @param message the message to set, or <code>null</code> to remove the current message
* @param textWidget the {@link Text} widget associated to the message.
*/
private final void setErrorMessage(String message, Text textWidget) {
if (message != null) {
setHasError(true);
getManagedForm().getMessageManager().addMessage(textWidget, message, null /* data */,
IMessageProvider.ERROR, textWidget);
} else {
setHasError(false);
getManagedForm().getMessageManager().removeMessage(textWidget, textWidget);
}
}
@Override
public String[] getPossibleValues(String prefix) {
// Compute a list of existing classes for content assist completion
IProject project = getProject();
if (project == null || mReferenceClass == null) {
return null;
}
try {
IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
IType type = javaProject.findType(mReferenceClass);
// Use sets because query sometimes repeats the same class
Set<String> libraryTypes = new HashSet<String>(80);
Set<String> localTypes = new HashSet<String>(30);
if (type != null) {
ITypeHierarchy hierarchy = type.newTypeHierarchy(new NullProgressMonitor());
IType[] allSubtypes = hierarchy.getAllSubtypes(type);
for (IType subType : allSubtypes) {
int flags = subType.getFlags();
if (Flags.isPublic(flags) && !Flags.isAbstract(flags)) {
String fqcn = subType.getFullyQualifiedName();
if (subType.getResource() != null) {
localTypes.add(fqcn);
} else {
libraryTypes.add(fqcn);
}
}
}
}
List<String> local = new ArrayList<String>(localTypes);
List<String> library = new ArrayList<String>(libraryTypes);
Collections.sort(local);
Collections.sort(library);
List<String> combined = new ArrayList<String>(local.size() + library.size());
combined.addAll(local);
combined.addAll(library);
return combined.toArray(new String[combined.size()]);
} catch (Exception e) {
AdtPlugin.log(e, null);
}
return null;
}
}