diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest')
26 files changed, 5816 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java new file mode 100644 index 000000000..1492adbb7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java @@ -0,0 +1,94 @@ +/* + * 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; + +import static com.android.xml.AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION; +import static com.android.xml.AndroidManifest.ATTRIBUTE_TARGET_SDK_VERSION; + +import com.android.annotations.VisibleForTesting; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.AndroidContentAssist; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.utils.Pair; + +import org.eclipse.jface.text.contentassist.ICompletionProposal; +import org.w3c.dom.Node; + +import java.util.ArrayList; +import java.util.List; + +/** + * Content Assist Processor for AndroidManifest.xml + */ +@VisibleForTesting +public final class ManifestContentAssist extends AndroidContentAssist { + + /** + * Constructor for ManifestContentAssist + */ + public ManifestContentAssist() { + super(AndroidTargetData.DESCRIPTOR_MANIFEST); + } + + @Override + protected boolean computeAttributeValues(List<ICompletionProposal> proposals, int offset, + String parentTagName, String attributeName, Node node, String wordPrefix, + boolean skipEndTag, int replaceLength) { + if (attributeName.endsWith(ATTRIBUTE_MIN_SDK_VERSION) + || attributeName.endsWith(ATTRIBUTE_TARGET_SDK_VERSION)) { + // The user is completing the minSdkVersion attribute: it should be + // an integer for the API version, but we'll add full Android version + // names to make it more obvious what they're selecting + + List<Pair<String, String>> choices = new ArrayList<Pair<String, String>>(); + int max = AdtUtils.getHighestKnownApiLevel(); + // Look for any more recent installed versions the user may have + Sdk sdk = Sdk.getCurrent(); + if (sdk == null) { + return false; + } + IAndroidTarget[] targets = sdk.getTargets(); + for (IAndroidTarget target : targets) { + AndroidVersion version = target.getVersion(); + int apiLevel = version.getApiLevel(); + if (apiLevel > max) { + if (version.isPreview()) { + // Use codename, not API level, as version string for preview versions + choices.add(Pair.of(version.getCodename(), version.getCodename())); + } else { + choices.add(Pair.of(Integer.toString(apiLevel), target.getFullName())); + } + } + } + for (int api = max; api >= 1; api--) { + String name = AdtUtils.getAndroidName(api); + choices.add(Pair.of(Integer.toString(api), name)); + } + char needTag = 0; + addMatchingProposals(proposals, choices.toArray(), offset, node, wordPrefix, + needTag, true /* isAttribute */, false /* isNew */, + skipEndTag /* skipEndTag */, replaceLength); + return true; + } else { + return super.computeAttributeValues(proposals, offset, parentTagName, attributeName, + node, wordPrefix, skipEndTag, replaceLength); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java new file mode 100644 index 000000000..55ebf5970 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java @@ -0,0 +1,578 @@ +/* + * 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; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors.USES_PERMISSION; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +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.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.adt.internal.editors.manifest.pages.ApplicationPage; +import com.android.ide.eclipse.adt.internal.editors.manifest.pages.InstrumentationPage; +import com.android.ide.eclipse.adt.internal.editors.manifest.pages.OverviewPage; +import com.android.ide.eclipse.adt.internal.editors.manifest.pages.PermissionPage; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IMarkerDelta; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.PartInitException; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.util.Collection; +import java.util.List; + +/** + * Multi-page form editor for AndroidManifest.xml. + */ +@SuppressWarnings("restriction") +public final class ManifestEditor extends AndroidXmlEditor { + + public static final String ID = AdtConstants.EDITORS_NAMESPACE + ".manifest.ManifestEditor"; //$NON-NLS-1$ + + private final static String EMPTY = ""; //$NON-NLS-1$ + + /** Root node of the UI element hierarchy */ + private UiElementNode mUiManifestNode; + /** The Application Page tab */ + private ApplicationPage mAppPage; + /** The Overview Manifest Page tab */ + private OverviewPage mOverviewPage; + /** The Permission Page tab */ + private PermissionPage mPermissionPage; + /** The Instrumentation Page tab */ + private InstrumentationPage mInstrumentationPage; + + private IFileListener mMarkerMonitor; + + + /** + * Creates the form editor for AndroidManifest.xml. + */ + public ManifestEditor() { + super(); + addDefaultTargetListener(); + } + + @Override + public void dispose() { + super.dispose(); + + GlobalProjectMonitor.getMonitor().removeFileListener(mMarkerMonitor); + } + + @Override + public void activated() { + super.activated(); + clearActionBindings(false); + } + + @Override + public void deactivated() { + super.deactivated(); + updateActionBindings(); + } + + @Override + protected void pageChange(int newPageIndex) { + super.pageChange(newPageIndex); + if (newPageIndex == mTextPageIndex) { + updateActionBindings(); + } else { + clearActionBindings(false); + } + } + + @Override + protected int getPersistenceCategory() { + return CATEGORY_MANIFEST; + } + + /** + * Return the root node of the UI element hierarchy, which here + * is the "manifest" node. + */ + @Override + public UiElementNode getUiRootNode() { + return mUiManifestNode; + } + + /** + * Returns the Manifest descriptors for the file being edited. + */ + public AndroidManifestDescriptors getManifestDescriptors() { + AndroidTargetData data = getTargetData(); + if (data != null) { + return data.getManifestDescriptors(); + } + + return null; + } + + // ---- Base Class Overrides ---- + + /** + * Returns whether the "save as" operation is supported by this editor. + * <p/> + * Save-As is a valid operation for the ManifestEditor since it acts on a + * single source file. + * + * @see IEditorPart + */ + @Override + public boolean isSaveAsAllowed() { + return true; + } + + @Override + public void doSave(IProgressMonitor monitor) { + // Look up the current (pre-save) values of minSdkVersion and targetSdkVersion + int prevMinSdkVersion = -1; + int prevTargetSdkVersion = -1; + IProject project = null; + ManifestInfo info = null; + try { + project = getProject(); + if (project != null) { + info = ManifestInfo.get(project); + prevMinSdkVersion = info.getMinSdkVersion(); + prevTargetSdkVersion = info.getTargetSdkVersion(); + info.clear(); + } + } catch (Throwable t) { + // We don't expect exceptions from the above calls, but we *really* + // need to make sure that nothing can prevent the save function from + // getting called! + AdtPlugin.log(t, null); + } + + // Actually save + super.doSave(monitor); + + // If the target/minSdkVersion has changed, clear all lint warnings (since many + // of them are tied to the min/target sdk levels), in order to avoid showing stale + // results + try { + if (info != null) { + int newMinSdkVersion = info.getMinSdkVersion(); + int newTargetSdkVersion = info.getTargetSdkVersion(); + if (newMinSdkVersion != prevMinSdkVersion + || newTargetSdkVersion != prevTargetSdkVersion) { + assert project != null; + EclipseLintClient.clearMarkers(project); + } + } + } catch (Throwable t) { + AdtPlugin.log(t, null); + } + } + + /** + * Creates the various form pages. + */ + @Override + protected void createFormPages() { + try { + addPage(mOverviewPage = new OverviewPage(this)); + addPage(mAppPage = new ApplicationPage(this)); + addPage(mPermissionPage = new PermissionPage(this)); + addPage(mInstrumentationPage = new InstrumentationPage(this)); + } catch (PartInitException e) { + AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$ + } + } + + /* (non-java doc) + * Change the tab/title name to include the project name. + */ + @Override + protected void setInput(IEditorInput input) { + super.setInput(input); + IFile inputFile = getInputFile(); + if (inputFile != null) { + startMonitoringMarkers(); + setPartName(String.format("%1$s Manifest", inputFile.getProject().getName())); + } + } + + /** + * Processes the new XML Model, which XML root node is given. + * + * @param xml_doc The XML document, if available, or null if none exists. + */ + @Override + protected void xmlModelChanged(Document xml_doc) { + // create the ui root node on demand. + initUiRootNode(false /*force*/); + + loadFromXml(xml_doc); + } + + private void loadFromXml(Document xmlDoc) { + mUiManifestNode.setXmlDocument(xmlDoc); + Node node = getManifestXmlNode(xmlDoc); + + if (node != null) { + // Refresh the manifest UI node and all its descendants + mUiManifestNode.loadFromXmlNode(node); + } + } + + private Node getManifestXmlNode(Document xmlDoc) { + if (xmlDoc != null) { + ElementDescriptor manifestDesc = mUiManifestNode.getDescriptor(); + String manifestXmlName = manifestDesc == null ? null : manifestDesc.getXmlName(); + assert manifestXmlName != null; + + if (manifestXmlName != null) { + Node node = xmlDoc.getDocumentElement(); + if (node != null && manifestXmlName.equals(node.getNodeName())) { + return node; + } + + for (node = xmlDoc.getFirstChild(); + node != null; + node = node.getNextSibling()) { + if (node.getNodeType() == Node.ELEMENT_NODE && + manifestXmlName.equals(node.getNodeName())) { + return node; + } + } + } + } + + return null; + } + + private void onDescriptorsChanged() { + IStructuredModel model = getModelForRead(); + if (model != null) { + try { + Node node = getManifestXmlNode(getXmlDocument(model)); + mUiManifestNode.reloadFromXmlNode(node); + } finally { + model.releaseFromRead(); + } + } + + if (mOverviewPage != null) { + mOverviewPage.refreshUiApplicationNode(); + } + + if (mAppPage != null) { + mAppPage.refreshUiApplicationNode(); + } + + if (mPermissionPage != null) { + mPermissionPage.refreshUiNode(); + } + + if (mInstrumentationPage != null) { + mInstrumentationPage.refreshUiNode(); + } + } + + /** + * Reads and processes the current markers and adds a listener for marker changes. + */ + private void startMonitoringMarkers() { + final IFile inputFile = getInputFile(); + if (inputFile != null) { + updateFromExistingMarkers(inputFile); + + mMarkerMonitor = new IFileListener() { + @Override + public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas, + int kind, @Nullable String extension, int flags, boolean isAndroidProject) { + if (isAndroidProject && file.equals(inputFile)) { + processMarkerChanges(markerDeltas); + } + } + }; + + GlobalProjectMonitor.getMonitor().addFileListener( + mMarkerMonitor, IResourceDelta.CHANGED); + } + } + + /** + * Processes the markers of the specified {@link IFile} and updates the error status of + * {@link UiElementNode}s and {@link UiAttributeNode}s. + * @param inputFile the file being edited. + */ + private void updateFromExistingMarkers(IFile inputFile) { + try { + // get the markers for the file + IMarker[] markers = inputFile.findMarkers( + AdtConstants.MARKER_ANDROID, true, IResource.DEPTH_ZERO); + + AndroidManifestDescriptors desc = getManifestDescriptors(); + if (desc != null) { + ElementDescriptor appElement = desc.getApplicationElement(); + + if (appElement != null && mUiManifestNode != null) { + UiElementNode appUiNode = mUiManifestNode.findUiChildNode( + appElement.getXmlName()); + List<UiElementNode> children = appUiNode.getUiChildren(); + + for (IMarker marker : markers) { + processMarker(marker, children, IResourceDelta.ADDED); + } + } + } + + } catch (CoreException e) { + // findMarkers can throw an exception, in which case, we'll do nothing. + } + } + + /** + * Processes a {@link IMarker} change. + * @param markerDeltas the list of {@link IMarkerDelta} + */ + private void processMarkerChanges(IMarkerDelta[] markerDeltas) { + AndroidManifestDescriptors descriptors = getManifestDescriptors(); + if (descriptors != null && descriptors.getApplicationElement() != null) { + UiElementNode app_ui_node = mUiManifestNode.findUiChildNode( + descriptors.getApplicationElement().getXmlName()); + List<UiElementNode> children = app_ui_node.getUiChildren(); + + for (IMarkerDelta markerDelta : markerDeltas) { + processMarker(markerDelta.getMarker(), children, markerDelta.getKind()); + } + } + } + + /** + * Processes a new/old/updated marker. + * @param marker The marker being added/removed/changed + * @param nodeList the list of activity/service/provider/receiver nodes. + * @param kind the change kind. Can be {@link IResourceDelta#ADDED}, + * {@link IResourceDelta#REMOVED}, or {@link IResourceDelta#CHANGED} + */ + private void processMarker(IMarker marker, List<UiElementNode> nodeList, int kind) { + // get the data from the marker + String nodeType = marker.getAttribute(AdtConstants.MARKER_ATTR_TYPE, EMPTY); + if (nodeType == EMPTY) { + return; + } + + String className = marker.getAttribute(AdtConstants.MARKER_ATTR_CLASS, EMPTY); + if (className == EMPTY) { + return; + } + + for (UiElementNode ui_node : nodeList) { + if (ui_node.getDescriptor().getXmlName().equals(nodeType)) { + for (UiAttributeNode attr : ui_node.getAllUiAttributes()) { + if (attr.getDescriptor().getXmlLocalName().equals( + AndroidManifestDescriptors.ANDROID_NAME_ATTR)) { + if (attr.getCurrentValue().equals(className)) { + if (kind == IResourceDelta.REMOVED) { + attr.setHasError(false); + } else { + attr.setHasError(true); + } + return; + } + } + } + } + } + } + + /** + * Creates the initial UI Root Node, including the known mandatory elements. + * @param force if true, a new UiManifestNode is recreated even if it already exists. + */ + @Override + protected void initUiRootNode(boolean force) { + // The manifest UI node is always created, even if there's no corresponding XML node. + if (mUiManifestNode != null && force == false) { + return; + } + + AndroidManifestDescriptors manifestDescriptor = getManifestDescriptors(); + + if (manifestDescriptor != null) { + ElementDescriptor manifestElement = manifestDescriptor.getManifestElement(); + mUiManifestNode = manifestElement.createUiNode(); + mUiManifestNode.setEditor(this); + + // Similarly, always create the /manifest/uses-sdk followed by /manifest/application + // (order of the elements now matters) + ElementDescriptor element = manifestDescriptor.getUsesSdkElement(); + boolean present = false; + for (UiElementNode ui_node : mUiManifestNode.getUiChildren()) { + if (ui_node.getDescriptor() == element) { + present = true; + break; + } + } + if (!present) { + mUiManifestNode.appendNewUiChild(element); + } + + element = manifestDescriptor.getApplicationElement(); + present = false; + for (UiElementNode ui_node : mUiManifestNode.getUiChildren()) { + if (ui_node.getDescriptor() == element) { + present = true; + break; + } + } + if (!present) { + mUiManifestNode.appendNewUiChild(element); + } + + onDescriptorsChanged(); + } else { + // create a dummy descriptor/uinode until we have real descriptors + ElementDescriptor desc = new ElementDescriptor("manifest", //$NON-NLS-1$ + "temporary descriptors due to missing decriptors", //$NON-NLS-1$ + null /*tooltip*/, null /*sdk_url*/, null /*attributes*/, + null /*children*/, false /*mandatory*/); + mUiManifestNode = desc.createUiNode(); + mUiManifestNode.setEditor(this); + } + } + + /** + * Adds the given set of permissions into the manifest file in the suitable + * location + * + * @param permissions permission fqcn's to be added + * @param show if true, show one or more of the newly added permissions + */ + public void addPermissions(@NonNull final List<String> permissions, final boolean show) { + wrapUndoEditXmlModel("Add permissions", new Runnable() { + @Override + public void run() { + // Ensure that the model is current: + initUiRootNode(true /*force*/); + UiElementNode root = getUiRootNode(); + + ElementDescriptor descriptor = getManifestDescriptors().getUsesPermissionElement(); + boolean shown = false; + for (String permission : permissions) { + // Find the first permission which sorts alphabetically laster than + // this permission (or the last permission, if none are after in the alphabet) + // and insert it there + int lastPermissionIndex = -1; + int nextPermissionIndex = -1; + int index = 0; + for (UiElementNode sibling : root.getUiChildren()) { + Node node = sibling.getXmlNode(); + if (node.getNodeName().equals(USES_PERMISSION)) { + lastPermissionIndex = index; + String name = ((Element) node).getAttributeNS(ANDROID_URI, ATTR_NAME); + if (permission.compareTo(name) < 0) { + nextPermissionIndex = index; + break; + } + } else if (node.getNodeName().equals("application")) { //$NON-NLS-1$ + // permissions should come before the application element + nextPermissionIndex = index; + break; + } + index++; + } + + if (nextPermissionIndex != -1) { + index = nextPermissionIndex; + } else if (lastPermissionIndex != -1) { + index = lastPermissionIndex + 1; + } else { + index = root.getUiChildren().size(); + } + UiElementNode usesPermission = root.insertNewUiChild(index, descriptor); + usesPermission.setAttributeValue(ATTR_NAME, ANDROID_URI, permission, + true /*override*/); + Node node = usesPermission.createXmlNode(); + if (show && !shown) { + shown = true; + if (node instanceof IndexedRegion && getInputFile() != null) { + IndexedRegion indexedRegion = (IndexedRegion) node; + IRegion region = new Region(indexedRegion.getStartOffset(), + indexedRegion.getEndOffset() - indexedRegion.getStartOffset()); + try { + AdtPlugin.openFile(getInputFile(), region, true /*show*/); + } catch (PartInitException e) { + AdtPlugin.log(e, null); + } + } else { + show(node); + } + } + } + } + }); + } + + /** + * Removes the permissions from the manifest editor + * + * @param permissions the permission fqcn's to be removed + */ + public void removePermissions(@NonNull final Collection<String> permissions) { + wrapUndoEditXmlModel("Remove permissions", new Runnable() { + @Override + public void run() { + // Ensure that the model is current: + initUiRootNode(true /*force*/); + UiElementNode root = getUiRootNode(); + + for (String permission : permissions) { + for (UiElementNode sibling : root.getUiChildren()) { + Node node = sibling.getXmlNode(); + if (node.getNodeName().equals(USES_PERMISSION)) { + String name = ((Element) node).getAttributeNS(ANDROID_URI, ATTR_NAME); + if (name.equals(permission)) { + sibling.deleteXmlNode(); + break; + } + } + } + } + } + }); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditorContributor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditorContributor.java new file mode 100644 index 000000000..8beca30b8 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditorContributor.java @@ -0,0 +1,100 @@ +/* + * 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; + +import org.eclipse.jface.action.IAction; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.actions.ActionFactory; +import org.eclipse.ui.ide.IDEActionFactory; +import org.eclipse.ui.part.MultiPageEditorActionBarContributor; +import org.eclipse.ui.texteditor.ITextEditor; +import org.eclipse.ui.texteditor.ITextEditorActionConstants; + +/** + * Manages the installation/deinstallation of global actions for multi-page + * editors. Responsible for the redirection of global actions to the active + * editor. Multi-page contributor replaces the contributors for the individual + * editors in the multi-page editor. + * + * TODO: Doesn't look like we need this. Remove it if not needed. + * @deprecated + */ +final class ManifestEditorContributor extends MultiPageEditorActionBarContributor { + private IEditorPart mActiveEditorPart; + + /** + * Creates a multi-page contributor. + * + * Marked as Private so it can't be instanciated. This is a cheap way to make sure + * it's not being used. As noted in constructor, should be removed if not used. + * @deprecated + */ + private ManifestEditorContributor() { + super(); + } + + /** + * Returns the action registed with the given text editor. + * + * @return IAction or null if editor is null. + */ + protected IAction getAction(ITextEditor editor, String actionID) { + return (editor == null ? null : editor.getAction(actionID)); + } + + /* + * (non-JavaDoc) Method declared in + * AbstractMultiPageEditorActionBarContributor. + */ + + @Override + public void setActivePage(IEditorPart part) { + if (mActiveEditorPart == part) + return; + + mActiveEditorPart = part; + + IActionBars actionBars = getActionBars(); + if (actionBars != null) { + + ITextEditor editor = + (part instanceof ITextEditor) ? (ITextEditor)part : null; + + actionBars.setGlobalActionHandler(ActionFactory.DELETE.getId(), + getAction(editor, ITextEditorActionConstants.DELETE)); + actionBars.setGlobalActionHandler(ActionFactory.UNDO.getId(), + getAction(editor, ITextEditorActionConstants.UNDO)); + actionBars.setGlobalActionHandler(ActionFactory.REDO.getId(), + getAction(editor, ITextEditorActionConstants.REDO)); + actionBars.setGlobalActionHandler(ActionFactory.CUT.getId(), + getAction(editor, ITextEditorActionConstants.CUT)); + actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), + getAction(editor, ITextEditorActionConstants.COPY)); + actionBars.setGlobalActionHandler(ActionFactory.PASTE.getId(), + getAction(editor, ITextEditorActionConstants.PASTE)); + actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), + getAction(editor, ITextEditorActionConstants.SELECT_ALL)); + actionBars.setGlobalActionHandler(ActionFactory.FIND.getId(), + getAction(editor, ITextEditorActionConstants.FIND)); + actionBars.setGlobalActionHandler( + IDEActionFactory.BOOKMARK.getId(), getAction(editor, + IDEActionFactory.BOOKMARK.getId())); + actionBars.updateActionBars(); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java new file mode 100644 index 000000000..6d2d1c1f2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestInfo.java @@ -0,0 +1,957 @@ +/* + * Copyright (C) 2011 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; + +import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; +import static com.android.SdkConstants.CLASS_ACTIVITY; +import static com.android.SdkConstants.NS_RESOURCES; +import static com.android.xml.AndroidManifest.ATTRIBUTE_ICON; +import static com.android.xml.AndroidManifest.ATTRIBUTE_LABEL; +import static com.android.xml.AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION; +import static com.android.xml.AndroidManifest.ATTRIBUTE_NAME; +import static com.android.xml.AndroidManifest.ATTRIBUTE_PACKAGE; +import static com.android.xml.AndroidManifest.ATTRIBUTE_PARENT_ACTIVITY_NAME; +import static com.android.xml.AndroidManifest.ATTRIBUTE_SUPPORTS_RTL; +import static com.android.xml.AndroidManifest.ATTRIBUTE_TARGET_SDK_VERSION; +import static com.android.xml.AndroidManifest.ATTRIBUTE_THEME; +import static com.android.xml.AndroidManifest.ATTRIBUTE_UI_OPTIONS; +import static com.android.xml.AndroidManifest.ATTRIBUTE_VALUE; +import static com.android.xml.AndroidManifest.NODE_ACTIVITY; +import static com.android.xml.AndroidManifest.NODE_METADATA; +import static com.android.xml.AndroidManifest.NODE_USES_SDK; +import static com.android.xml.AndroidManifest.VALUE_PARENT_ACTIVITY; +import static org.eclipse.jdt.core.search.IJavaSearchConstants.REFERENCES; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.io.IFolderWrapper; +import com.android.io.IAbstractFile; +import com.android.io.StreamException; +import com.android.resources.ScreenSize; +import com.android.sdklib.IAndroidTarget; +import com.android.utils.Pair; +import com.android.xml.AndroidManifest; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.jdt.core.IField; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IMethod; +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.search.IJavaSearchScope; +import org.eclipse.jdt.core.search.SearchEngine; +import org.eclipse.jdt.core.search.SearchMatch; +import org.eclipse.jdt.core.search.SearchParticipant; +import org.eclipse.jdt.core.search.SearchPattern; +import org.eclipse.jdt.core.search.SearchRequestor; +import org.eclipse.jdt.internal.core.BinaryType; +import org.eclipse.jface.text.IDocument; +import org.eclipse.ui.editors.text.TextFileDocumentProvider; +import org.eclipse.ui.texteditor.IDocumentProvider; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPathExpressionException; + +/** + * Retrieves and caches manifest information such as the themes to be used for + * a given activity. + * + * @see AndroidManifest + */ +public class ManifestInfo { + + public static class ActivityAttributes { + @Nullable + private final String mIcon; + @Nullable + private final String mLabel; + @NonNull + private final String mName; + @Nullable + private final String mParentActivity; + @Nullable + private final String mTheme; + @Nullable + private final String mUiOptions; + + public ActivityAttributes(Element activity, String packageName) { + + // Get activity name. + String name = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_NAME); + if (name == null || name.length() == 0) { + throw new RuntimeException("Activity name cannot be empty"); + } + int index = name.indexOf('.'); + if (index <= 0 && packageName != null && !packageName.isEmpty()) { + name = packageName + (index == -1 ? "." : "") + name; + } + mName = name; + + // Get activity icon. + String value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON); + if (value != null && value.length() > 0) { + mIcon = value; + } else { + mIcon = null; + } + + // Get activity label. + value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL); + if (value != null && value.length() > 0) { + mLabel = value; + } else { + mLabel = null; + } + + // Get activity parent. Also search the meta-data for parent info. + value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_PARENT_ACTIVITY_NAME); + if (value == null || value.length() == 0) { + // TODO: Not sure if meta data can be used for API Level > 16 + NodeList metaData = activity.getElementsByTagName(NODE_METADATA); + for (int j = 0, m = metaData.getLength(); j < m; j++) { + Element data = (Element) metaData.item(j); + String metadataName = data.getAttributeNS(NS_RESOURCES, ATTRIBUTE_NAME); + if (VALUE_PARENT_ACTIVITY.equals(metadataName)) { + value = data.getAttributeNS(NS_RESOURCES, ATTRIBUTE_VALUE); + if (value != null) { + index = value.indexOf('.'); + if (index <= 0 && packageName != null && !packageName.isEmpty()) { + value = packageName + (index == -1 ? "." : "") + value; + break; + } + } + } + } + } + if (value != null && value.length() > 0) { + mParentActivity = value; + } else { + mParentActivity = null; + } + + // Get activity theme. + value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_THEME); + if (value != null && value.length() > 0) { + mTheme = value; + } else { + mTheme = null; + } + + // Get UI options. + value = activity.getAttributeNS(NS_RESOURCES, ATTRIBUTE_UI_OPTIONS); + if (value != null && value.length() > 0) { + mUiOptions = value; + } else { + mUiOptions = null; + } + } + + @Nullable + public String getIcon() { + return mIcon; + } + + @Nullable + public String getLabel() { + return mLabel; + } + + public String getName() { + return mName; + } + + @Nullable + public String getParentActivity() { + return mParentActivity; + } + + @Nullable + public String getTheme() { + return mTheme; + } + + @Nullable + public String getUiOptions() { + return mUiOptions; + } + } + + /** + * The maximum number of milliseconds to search for an activity in the codebase when + * attempting to associate layouts with activities in + * {@link #guessActivity(IFile, String)} + */ + private static final int SEARCH_TIMEOUT_MS = 3000; + + private final IProject mProject; + private String mPackage; + private String mManifestTheme; + private Map<String, ActivityAttributes> mActivityAttributes; + private IAbstractFile mManifestFile; + private long mLastModified; + private long mLastChecked; + private String mMinSdkName; + private int mMinSdk; + private int mTargetSdk; + private String mApplicationIcon; + private String mApplicationLabel; + private boolean mApplicationSupportsRtl; + + /** + * Qualified name for the per-project non-persistent property storing the + * {@link ManifestInfo} for this project + */ + final static QualifiedName MANIFEST_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID, + "manifest"); //$NON-NLS-1$ + + /** + * Constructs an {@link ManifestInfo} for the given project. Don't use this method; + * use the {@link #get} factory method instead. + * + * @param project project to create an {@link ManifestInfo} for + */ + private ManifestInfo(IProject project) { + mProject = project; + } + + /** + * Clears the cached manifest information. The next get call on one of the + * properties will cause the information to be refreshed. + */ + public void clear() { + mLastChecked = 0; + } + + /** + * Returns the {@link ManifestInfo} for the given project + * + * @param project the project the finder is associated with + * @return a {@ManifestInfo} for the given project, never null + */ + @NonNull + public static ManifestInfo get(IProject project) { + ManifestInfo finder = null; + try { + finder = (ManifestInfo) project.getSessionProperty(MANIFEST_FINDER); + } catch (CoreException e) { + // Not a problem; we will just create a new one + } + + if (finder == null) { + finder = new ManifestInfo(project); + try { + project.setSessionProperty(MANIFEST_FINDER, finder); + } catch (CoreException e) { + AdtPlugin.log(e, "Can't store ManifestInfo"); + } + } + + return finder; + } + + /** + * Ensure that the package, theme and activity maps are initialized and up to date + * with respect to the manifest file + */ + private void sync() { + // Since each of the accessors call sync(), allow a bunch of immediate + // accessors to all bypass the file stat() below + long now = System.currentTimeMillis(); + if (now - mLastChecked < 50 && mManifestFile != null) { + return; + } + mLastChecked = now; + + if (mManifestFile == null) { + IFolderWrapper projectFolder = new IFolderWrapper(mProject); + mManifestFile = AndroidManifest.getManifest(projectFolder); + if (mManifestFile == null) { + return; + } + } + + // Check to see if our data is up to date + long fileModified = mManifestFile.getModificationStamp(); + if (fileModified == mLastModified) { + // Already have up to date data + return; + } + mLastModified = fileModified; + + mActivityAttributes = new HashMap<String, ActivityAttributes>(); + mManifestTheme = null; + mTargetSdk = 1; // Default when not specified + mMinSdk = 1; // Default when not specified + mMinSdkName = "1"; // Default when not specified + mPackage = ""; //$NON-NLS-1$ + mApplicationIcon = null; + mApplicationLabel = null; + mApplicationSupportsRtl = false; + + Document document = null; + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + InputSource is = new InputSource(mManifestFile.getContents()); + + factory.setNamespaceAware(true); + factory.setValidating(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + document = builder.parse(is); + + Element root = document.getDocumentElement(); + mPackage = root.getAttribute(ATTRIBUTE_PACKAGE); + NodeList activities = document.getElementsByTagName(NODE_ACTIVITY); + for (int i = 0, n = activities.getLength(); i < n; i++) { + Element activity = (Element) activities.item(i); + ActivityAttributes info = new ActivityAttributes(activity, mPackage); + mActivityAttributes.put(info.getName(), info); + } + + NodeList applications = root.getElementsByTagName(AndroidManifest.NODE_APPLICATION); + if (applications.getLength() > 0) { + assert applications.getLength() == 1; + Element application = (Element) applications.item(0); + if (application.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON)) { + mApplicationIcon = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_ICON); + } + if (application.hasAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL)) { + mApplicationLabel = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_LABEL); + } + if (SdkConstants.VALUE_TRUE.equals(application.getAttributeNS(NS_RESOURCES, + ATTRIBUTE_SUPPORTS_RTL))) { + mApplicationSupportsRtl = true; + } + + String defaultTheme = application.getAttributeNS(NS_RESOURCES, ATTRIBUTE_THEME); + if (defaultTheme != null && !defaultTheme.isEmpty()) { + // From manifest theme documentation: + // "If that attribute is also not set, the default system theme is used." + mManifestTheme = defaultTheme; + } + } + + // Look up target SDK + NodeList usesSdks = root.getElementsByTagName(NODE_USES_SDK); + if (usesSdks.getLength() > 0) { + Element usesSdk = (Element) usesSdks.item(0); + mMinSdk = getApiVersion(usesSdk, ATTRIBUTE_MIN_SDK_VERSION, 1); + mTargetSdk = getApiVersion(usesSdk, ATTRIBUTE_TARGET_SDK_VERSION, mMinSdk); + } + + } catch (SAXException e) { + AdtPlugin.log(e, "Malformed manifest"); + } catch (Exception e) { + AdtPlugin.log(e, "Could not read Manifest data"); + } + } + + private int getApiVersion(Element usesSdk, String attribute, int defaultApiLevel) { + String valueString = null; + if (usesSdk.hasAttributeNS(NS_RESOURCES, attribute)) { + valueString = usesSdk.getAttributeNS(NS_RESOURCES, attribute); + if (attribute.equals(ATTRIBUTE_MIN_SDK_VERSION)) { + mMinSdkName = valueString; + } + } + + if (valueString != null) { + int apiLevel = -1; + try { + apiLevel = Integer.valueOf(valueString); + } catch (NumberFormatException e) { + // Handle codename + if (Sdk.getCurrent() != null) { + IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString( + "android-" + valueString); //$NON-NLS-1$ + if (target != null) { + // codename future API level is current api + 1 + apiLevel = target.getVersion().getApiLevel() + 1; + } + } + } + + return apiLevel; + } + + return defaultApiLevel; + } + + /** + * Returns the default package registered in the Android manifest + * + * @return the default package registered in the manifest + */ + @NonNull + public String getPackage() { + sync(); + return mPackage; + } + + /** + * Returns a map from activity full class names to the corresponding {@link ActivityAttributes}. + * + * @return a map from activity fqcn to ActivityAttributes + */ + @NonNull + public Map<String, ActivityAttributes> getActivityAttributesMap() { + sync(); + return mActivityAttributes; + } + + /** + * Returns the attributes of an activity given its full class name. + */ + @Nullable + public ActivityAttributes getActivityAttributes(String activity) { + return getActivityAttributesMap().get(activity); + } + + /** + * Returns the manifest theme registered on the application, if any + * + * @return a manifest theme, or null if none was registered + */ + @Nullable + public String getManifestTheme() { + sync(); + return mManifestTheme; + } + + /** + * Returns the default theme for this project, by looking at the manifest default + * theme registration, target SDK, rendering target, etc. + * + * @param renderingTarget the rendering target use to render the theme, or null + * @param screenSize the screen size to obtain a default theme for, or null if unknown + * @return the theme to use for this project, never null + */ + @NonNull + public String getDefaultTheme(IAndroidTarget renderingTarget, ScreenSize screenSize) { + sync(); + + if (mManifestTheme != null) { + return mManifestTheme; + } + + int renderingTargetSdk = mTargetSdk; + if (renderingTarget != null) { + renderingTargetSdk = renderingTarget.getVersion().getApiLevel(); + } + + int apiLevel = Math.min(mTargetSdk, renderingTargetSdk); + // For now this theme works only on XLARGE screens. When it works for all sizes, + // add that new apiLevel to this check. + if (apiLevel >= 11 && screenSize == ScreenSize.XLARGE || apiLevel >= 14) { + return ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Holo"; //$NON-NLS-1$ + } else { + return ANDROID_STYLE_RESOURCE_PREFIX + "Theme"; //$NON-NLS-1$ + } + } + + /** + * Returns the application icon, or null + * + * @return the application icon, or null + */ + @Nullable + public String getApplicationIcon() { + sync(); + return mApplicationIcon; + } + + /** + * Returns the application label, or null + * + * @return the application label, or null + */ + @Nullable + public String getApplicationLabel() { + sync(); + return mApplicationLabel; + } + + /** + * Returns true if the application has RTL support. + * + * @return true if the application has RTL support. + */ + public boolean isRtlSupported() { + sync(); + return mApplicationSupportsRtl; + } + + /** + * Returns the target SDK version + * + * @return the target SDK version + */ + public int getTargetSdkVersion() { + sync(); + return mTargetSdk; + } + + /** + * Returns the minimum SDK version + * + * @return the minimum SDK version + */ + public int getMinSdkVersion() { + sync(); + return mMinSdk; + } + + /** + * Returns the minimum SDK version name (which may not be a numeric string, e.g. + * it could be a codename). It will never be null or empty; if no min sdk version + * was specified in the manifest, the return value will be "1". Use + * {@link #getMinSdkCodeName()} instead if you want to look up whether there is a code name. + * + * @return the minimum SDK version + */ + @NonNull + public String getMinSdkName() { + sync(); + if (mMinSdkName == null || mMinSdkName.isEmpty()) { + mMinSdkName = "1"; //$NON-NLS-1$ + } + + return mMinSdkName; + } + + /** + * Returns the code name used for the minimum SDK version, if any. + * + * @return the minSdkVersion codename or null + */ + @Nullable + public String getMinSdkCodeName() { + String minSdkName = getMinSdkName(); + if (!Character.isDigit(minSdkName.charAt(0))) { + return minSdkName; + } + + return null; + } + + /** + * Returns the {@link IPackageFragment} for the package registered in the manifest + * + * @return the {@link IPackageFragment} for the package registered in the manifest + */ + @Nullable + public IPackageFragment getPackageFragment() { + sync(); + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject); + if (javaProject != null) { + IPackageFragmentRoot root = ManifestInfo.getSourcePackageRoot(javaProject); + if (root != null) { + return root.getPackageFragment(mPackage); + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return null; + } + + /** + * Returns the activity associated with the given layout file. Makes an educated guess + * by peeking at the usages of the R.layout.name field corresponding to the layout and + * if it finds a usage. + * + * @param project the project containing the layout + * @param layoutName the layout whose activity we want to look up + * @param pkg the package containing activities + * @return the activity name + */ + @Nullable + public static String guessActivity(IProject project, String layoutName, String pkg) { + List<String> activities = guessActivities(project, layoutName, pkg); + if (activities.size() > 0) { + return activities.get(0); + } else { + return null; + } + } + + /** + * Returns the activities associated with the given layout file. Makes an educated guess + * by peeking at the usages of the R.layout.name field corresponding to the layout and + * if it finds a usage. + * + * @param project the project containing the layout + * @param layoutName the layout whose activity we want to look up + * @param pkg the package containing activities + * @return the activity name + */ + @NonNull + public static List<String> guessActivities(IProject project, String layoutName, String pkg) { + final LinkedList<String> activities = new LinkedList<String>(); + SearchRequestor requestor = new SearchRequestor() { + @Override + public void acceptSearchMatch(SearchMatch match) throws CoreException { + Object element = match.getElement(); + if (element instanceof IMethod) { + IMethod method = (IMethod) element; + IType declaringType = method.getDeclaringType(); + String fqcn = declaringType.getFullyQualifiedName(); + + if ((declaringType.getSuperclassName() != null && + declaringType.getSuperclassName().endsWith("Activity")) //$NON-NLS-1$ + || method.getElementName().equals("onCreate")) { //$NON-NLS-1$ + activities.addFirst(fqcn); + } else { + activities.addLast(fqcn); + } + } + } + }; + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + if (javaProject == null) { + return Collections.emptyList(); + } + // TODO - look around a bit more and see if we can figure out whether the + // call if from within a setContentView call! + + // Search for which java classes call setContentView(R.layout.layoutname); + String typeFqcn = "R.layout"; //$NON-NLS-1$ + if (pkg != null) { + typeFqcn = pkg + '.' + typeFqcn; + } + + IType type = javaProject.findType(typeFqcn); + if (type != null) { + IField field = type.getField(layoutName); + if (field.exists()) { + SearchPattern pattern = SearchPattern.createPattern(field, REFERENCES); + try { + search(requestor, javaProject, pattern); + } catch (OperationCanceledException canceled) { + // pass + } + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return activities; + } + + /** + * Returns all activities found in the given project (including those in libraries, + * except for android.jar itself) + * + * @param project the project + * @return a list of activity classes as fully qualified class names + */ + @SuppressWarnings("restriction") // BinaryType + @NonNull + public static List<String> getProjectActivities(IProject project) { + final List<String> activities = new ArrayList<String>(); + try { + final IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + if (javaProject != null) { + IType[] activityTypes = new IType[0]; + IType activityType = javaProject.findType(CLASS_ACTIVITY); + if (activityType != null) { + ITypeHierarchy hierarchy = + activityType.newTypeHierarchy(javaProject, new NullProgressMonitor()); + activityTypes = hierarchy.getAllSubtypes(activityType); + for (IType type : activityTypes) { + if (type instanceof BinaryType && (type.getClassFile() == null + || type.getClassFile().getResource() == null)) { + continue; + } + activities.add(type.getFullyQualifiedName()); + } + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return activities; + } + + + /** + * Returns the activity associated with the given layout file. + * <p> + * This is an alternative to {@link #guessActivity(IFile, String)}. Whereas + * guessActivity simply looks for references to "R.layout.foo", this method searches + * for all usages of Activity#setContentView(int), and for each match it looks up the + * corresponding call text (such as "setContentView(R.layout.foo)"). From this it uses + * a regexp to pull out "foo" from this, and stores the association that layout "foo" + * is associated with the activity class that contained the setContentView call. + * <p> + * This has two potential advantages: + * <ol> + * <li>It can be faster. We do the reference search -once-, and we've built a map of + * all the layout-to-activity mappings which we can then immediately look up other + * layouts for, which is particularly useful at startup when we have to compute the + * layout activity associations to populate the theme choosers. + * <li>It can be more accurate. Just because an activity references an "R.layout.foo" + * field doesn't mean it's setting it as a content view. + * </ol> + * However, this second advantage is also its chief problem. There are some common + * code constructs which means that the associated layout is not explicitly referenced + * in a direct setContentView call; on a couple of sample projects I tested I found + * patterns like for example "setContentView(v)" where "v" had been computed earlier. + * Therefore, for now we're going to stick with the more general approach of just + * looking up each field when needed. We're keeping the code around, though statically + * compiled out with the "if (false)" construct below in case we revisit this. + * + * @param layoutFile the layout whose activity we want to look up + * @return the activity name + */ + @SuppressWarnings("all") + @Nullable + public String guessActivityBySetContentView(String layoutName) { + if (false) { + // These should be fields + final Pattern LAYOUT_FIELD_PATTERN = + Pattern.compile("R\\.layout\\.([a-z0-9_]+)"); //$NON-NLS-1$ + Map<String, String> mUsages = null; + + sync(); + if (mUsages == null) { + final Map<String, String> usages = new HashMap<String, String>(); + mUsages = usages; + SearchRequestor requestor = new SearchRequestor() { + @Override + public void acceptSearchMatch(SearchMatch match) throws CoreException { + Object element = match.getElement(); + if (element instanceof IMethod) { + IMethod method = (IMethod) element; + IType declaringType = method.getDeclaringType(); + String fqcn = declaringType.getFullyQualifiedName(); + IDocumentProvider provider = new TextFileDocumentProvider(); + IResource resource = match.getResource(); + try { + provider.connect(resource); + IDocument document = provider.getDocument(resource); + if (document != null) { + String matchText = document.get(match.getOffset(), + match.getLength()); + Matcher matcher = LAYOUT_FIELD_PATTERN.matcher(matchText); + if (matcher.find()) { + usages.put(matcher.group(1), fqcn); + } + } + } catch (Exception e) { + AdtPlugin.log(e, "Can't find range information for %1$s", + resource.getName()); + } finally { + provider.disconnect(resource); + } + } + } + }; + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject); + if (javaProject == null) { + return null; + } + + // Search for which java classes call setContentView(R.layout.layoutname); + String typeFqcn = "R.layout"; //$NON-NLS-1$ + if (mPackage != null) { + typeFqcn = mPackage + '.' + typeFqcn; + } + + IType activityType = javaProject.findType(CLASS_ACTIVITY); + if (activityType != null) { + IMethod method = activityType.getMethod( + "setContentView", new String[] {"I"}); //$NON-NLS-1$ //$NON-NLS-2$ + if (method.exists()) { + SearchPattern pattern = SearchPattern.createPattern(method, + REFERENCES); + search(requestor, javaProject, pattern); + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + + return mUsages.get(layoutName); + } + + return null; + } + + /** + * Performs a search using the given pattern, scope and handler. The search will abort + * if it takes longer than {@link #SEARCH_TIMEOUT_MS} milliseconds. + */ + private static void search(SearchRequestor requestor, IJavaProject javaProject, + SearchPattern pattern) throws CoreException { + // Find the package fragment specified in the manifest; the activities should + // live there. + IJavaSearchScope scope = createPackageScope(javaProject); + + SearchParticipant[] participants = new SearchParticipant[] { + SearchEngine.getDefaultSearchParticipant() + }; + SearchEngine engine = new SearchEngine(); + + final long searchStart = System.currentTimeMillis(); + NullProgressMonitor monitor = new NullProgressMonitor() { + private boolean mCancelled; + @Override + public void internalWorked(double work) { + long searchEnd = System.currentTimeMillis(); + if (searchEnd - searchStart > SEARCH_TIMEOUT_MS) { + mCancelled = true; + } + } + + @Override + public boolean isCanceled() { + return mCancelled; + } + }; + engine.search(pattern, participants, scope, requestor, monitor); + } + + /** Creates a package search scope for the first package root in the given java project */ + private static IJavaSearchScope createPackageScope(IJavaProject javaProject) { + IPackageFragmentRoot packageRoot = getSourcePackageRoot(javaProject); + + IJavaSearchScope scope; + if (packageRoot != null) { + IJavaElement[] scopeElements = new IJavaElement[] { packageRoot }; + scope = SearchEngine.createJavaSearchScope(scopeElements); + } else { + scope = SearchEngine.createWorkspaceScope(); + } + return scope; + } + + /** + * Returns the first package root for the given java project + * + * @param javaProject the project to search in + * @return the first package root, or null + */ + @Nullable + public static IPackageFragmentRoot getSourcePackageRoot(IJavaProject javaProject) { + IPackageFragmentRoot packageRoot = null; + List<IPath> sources = BaseProjectHelper.getSourceClasspaths(javaProject); + + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + for (IPath path : sources) { + IResource firstSource = workspace.getRoot().findMember(path); + if (firstSource != null) { + packageRoot = javaProject.getPackageFragmentRoot(firstSource); + if (packageRoot != null) { + break; + } + } + } + return packageRoot; + } + + /** + * Computes the minimum SDK and target SDK versions for the project + * + * @param project the project to look up the versions for + * @return a pair of (minimum SDK, target SDK) versions, never null + */ + @NonNull + public static Pair<Integer, Integer> computeSdkVersions(IProject project) { + int mMinSdkVersion = 1; + int mTargetSdkVersion = 1; + + IAbstractFile manifestFile = AndroidManifest.getManifest(new IFolderWrapper(project)); + if (manifestFile != null) { + try { + Object value = AndroidManifest.getMinSdkVersion(manifestFile); + mMinSdkVersion = 1; // Default case if missing + if (value instanceof Integer) { + mMinSdkVersion = ((Integer) value).intValue(); + } else if (value instanceof String) { + // handle codename, only if we can resolve it. + if (Sdk.getCurrent() != null) { + IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString( + "android-" + value); //$NON-NLS-1$ + if (target != null) { + // codename future API level is current api + 1 + mMinSdkVersion = target.getVersion().getApiLevel() + 1; + } + } + } + + value = AndroidManifest.getTargetSdkVersion(manifestFile); + if (value == null) { + mTargetSdkVersion = mMinSdkVersion; + } else if (value instanceof String) { + // handle codename, only if we can resolve it. + if (Sdk.getCurrent() != null) { + IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString( + "android-" + value); //$NON-NLS-1$ + if (target != null) { + // codename future API level is current api + 1 + mTargetSdkVersion = target.getVersion().getApiLevel() + 1; + } + } + } + } catch (XPathExpressionException e) { + // do nothing we'll use 1 below. + } catch (StreamException e) { + // do nothing we'll use 1 below. + } + } + + return Pair.of(mMinSdkVersion, mTargetSdkVersion); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestSourceViewerConfig.java new file mode 100644 index 000000000..a3d398657 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestSourceViewerConfig.java @@ -0,0 +1,43 @@ +/* + * 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; + + +import com.android.ide.eclipse.adt.internal.editors.AndroidSourceViewerConfig; + +import org.eclipse.jface.text.contentassist.IContentAssistProcessor; +import org.eclipse.jface.text.source.ISourceViewer; + +/** + * Source Viewer Configuration that calls in ManifestContentAssist. + */ +public final class ManifestSourceViewerConfig extends AndroidSourceViewerConfig { + + private ManifestContentAssist mAndroidContentAssist; + + public ManifestSourceViewerConfig() { + super(); + mAndroidContentAssist = new ManifestContentAssist(); + } + + @Override + public IContentAssistProcessor getAndroidContentAssistProcessor( + ISourceViewer sourceViewer, + String partitionType) { + return mAndroidContentAssist; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/AndroidManifestDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/AndroidManifestDescriptors.java new file mode 100644 index 000000000..3429e43a0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/AndroidManifestDescriptors.java @@ -0,0 +1,628 @@ +/* + * 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.descriptors; + +import com.android.SdkConstants; +import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.common.api.IAttributeInfo.Format; +import com.android.ide.common.resources.platform.AttributeInfo; +import com.android.ide.common.resources.platform.AttrsXmlParser; +import com.android.ide.common.resources.platform.DeclareStyleableInfo; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor.Mandatory; +import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ITextAttributeCreator; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ListAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ReferenceAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor; + +import org.eclipse.core.runtime.IStatus; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; + + +/** + * Complete description of the AndroidManifest.xml structure. + * <p/> + * The root element are static instances which always exists. + * However their sub-elements and attributes are created only when the SDK changes or is + * loaded the first time. + */ +public final class AndroidManifestDescriptors implements IDescriptorProvider { + /** Name of the {@code <uses-permission>} */ + public static final String USES_PERMISSION = "uses-permission"; //$NON-NLS-1$ + private static final String MANIFEST_NODE_NAME = "manifest"; //$NON-NLS-1$ + private static final String ANDROID_MANIFEST_STYLEABLE = + AttrsXmlParser.ANDROID_MANIFEST_STYLEABLE; + + // Public attributes names, attributes descriptors and elements descriptors + + public static final String ANDROID_LABEL_ATTR = "label"; //$NON-NLS-1$ + public static final String ANDROID_NAME_ATTR = "name"; //$NON-NLS-1$ + public static final String PACKAGE_ATTR = "package"; //$NON-NLS-1$ + + /** The {@link ElementDescriptor} for the root Manifest element. */ + private final ElementDescriptor MANIFEST_ELEMENT; + /** The {@link ElementDescriptor} for the root Application element. */ + private final ElementDescriptor APPLICATION_ELEMENT; + + /** The {@link ElementDescriptor} for the root Instrumentation element. */ + private final ElementDescriptor INTRUMENTATION_ELEMENT; + /** The {@link ElementDescriptor} for the root Permission element. */ + private final ElementDescriptor PERMISSION_ELEMENT; + /** The {@link ElementDescriptor} for the root UsesPermission element. */ + private final ElementDescriptor USES_PERMISSION_ELEMENT; + /** The {@link ElementDescriptor} for the root UsesSdk element. */ + private final ElementDescriptor USES_SDK_ELEMENT; + + /** The {@link ElementDescriptor} for the root PermissionGroup element. */ + private final ElementDescriptor PERMISSION_GROUP_ELEMENT; + /** The {@link ElementDescriptor} for the root PermissionTree element. */ + private final ElementDescriptor PERMISSION_TREE_ELEMENT; + + /** Private package attribute for the manifest element. Needs to be handled manually. */ + private final TextAttributeDescriptor PACKAGE_ATTR_DESC; + + public AndroidManifestDescriptors() { + APPLICATION_ELEMENT = createElement("application", null, Mandatory.MANDATORY_LAST); //$NON-NLS-1$ + no child & mandatory + INTRUMENTATION_ELEMENT = createElement("instrumentation"); //$NON-NLS-1$ + + PERMISSION_ELEMENT = createElement("permission"); //$NON-NLS-1$ + USES_PERMISSION_ELEMENT = createElement(USES_PERMISSION); + USES_SDK_ELEMENT = createElement("uses-sdk", null, Mandatory.MANDATORY); //$NON-NLS-1$ + no child & mandatory + + PERMISSION_GROUP_ELEMENT = createElement("permission-group"); //$NON-NLS-1$ + PERMISSION_TREE_ELEMENT = createElement("permission-tree"); //$NON-NLS-1$ + + MANIFEST_ELEMENT = createElement( + MANIFEST_NODE_NAME, // xml name + new ElementDescriptor[] { + APPLICATION_ELEMENT, + INTRUMENTATION_ELEMENT, + PERMISSION_ELEMENT, + USES_PERMISSION_ELEMENT, + PERMISSION_GROUP_ELEMENT, + PERMISSION_TREE_ELEMENT, + USES_SDK_ELEMENT, + }, + Mandatory.MANDATORY); + + // The "package" attribute is treated differently as it doesn't have the standard + // Android XML namespace. + PACKAGE_ATTR_DESC = new PackageAttributeDescriptor(PACKAGE_ATTR, + null /* nsUri */, + new AttributeInfo(PACKAGE_ATTR, Format.REFERENCE_SET)).setTooltip( + "This attribute gives a unique name for the package, using a Java-style " + + "naming convention to avoid name collisions.\nFor example, applications " + + "published by Google could have names of the form com.google.app.appname"); + } + + @Override + public ElementDescriptor[] getRootElementDescriptors() { + return new ElementDescriptor[] { MANIFEST_ELEMENT }; + } + + @Override + public ElementDescriptor getDescriptor() { + return getManifestElement(); + } + + public ElementDescriptor getApplicationElement() { + return APPLICATION_ELEMENT; + } + + public ElementDescriptor getManifestElement() { + return MANIFEST_ELEMENT; + } + + public ElementDescriptor getUsesSdkElement() { + return USES_SDK_ELEMENT; + } + + public ElementDescriptor getInstrumentationElement() { + return INTRUMENTATION_ELEMENT; + } + + public ElementDescriptor getPermissionElement() { + return PERMISSION_ELEMENT; + } + + public ElementDescriptor getUsesPermissionElement() { + return USES_PERMISSION_ELEMENT; + } + + public ElementDescriptor getPermissionGroupElement() { + return PERMISSION_GROUP_ELEMENT; + } + + public ElementDescriptor getPermissionTreeElement() { + return PERMISSION_TREE_ELEMENT; + } + + /** + * Updates the document descriptor. + * <p/> + * It first computes the new children of the descriptor and then updates them + * all at once. + * + * @param manifestMap The map style => attributes from the attrs_manifest.xml file + */ + public synchronized void updateDescriptors( + Map<String, DeclareStyleableInfo> manifestMap) { + + // -- setup the required attributes overrides -- + + Set<String> required = new HashSet<String>(); + required.add("provider/authorities"); //$NON-NLS-1$ + + // -- setup the various attribute format overrides -- + + // The key for each override is "element1,element2,.../attr-xml-local-name" or + // "*/attr-xml-local-name" to match the attribute in any element. + + Map<String, ITextAttributeCreator> overrides = new HashMap<String, ITextAttributeCreator>(); + + overrides.put("*/icon", ReferenceAttributeDescriptor.CREATOR); //$NON-NLS-1$ + + overrides.put("*/theme", ThemeAttributeDescriptor.CREATOR); //$NON-NLS-1$ + overrides.put("*/permission", ListAttributeDescriptor.CREATOR); //$NON-NLS-1$ + overrides.put("*/targetPackage", ManifestPkgAttrDescriptor.CREATOR); //$NON-NLS-1$ + + overrides.put("uses-library/name", ListAttributeDescriptor.CREATOR); //$NON-NLS-1$ + overrides.put("action,category,uses-permission/" + ANDROID_NAME_ATTR, //$NON-NLS-1$ + ListAttributeDescriptor.CREATOR); + + overrideClassName(overrides, "application", //$NON-NLS-1$ + SdkConstants.CLASS_APPLICATION, + false /*mandatory*/); + overrideClassName(overrides, "application/backupAgent", //$NON-NLS-1$ + "android.app.backup.BackupAgent", //$NON-NLS-1$ + false /*mandatory*/); + overrideClassName(overrides, "activity", SdkConstants.CLASS_ACTIVITY); //$NON-NLS-1$ + overrideClassName(overrides, "receiver", SdkConstants.CLASS_BROADCASTRECEIVER);//$NON-NLS-1$ + overrideClassName(overrides, "service", SdkConstants.CLASS_SERVICE); //$NON-NLS-1$ + overrideClassName(overrides, "provider", SdkConstants.CLASS_CONTENTPROVIDER); //$NON-NLS-1$ + overrideClassName(overrides, "instrumentation", + SdkConstants.CLASS_INSTRUMENTATION); //$NON-NLS-1$ + + // -- list element nodes already created -- + // These elements are referenced by already opened editors, so we want to update them + // but not re-create them when reloading an SDK on the fly. + + HashMap<String, ElementDescriptor> elementDescs = + new HashMap<String, ElementDescriptor>(); + elementDescs.put(MANIFEST_ELEMENT.getXmlLocalName(), MANIFEST_ELEMENT); + elementDescs.put(APPLICATION_ELEMENT.getXmlLocalName(), APPLICATION_ELEMENT); + elementDescs.put(INTRUMENTATION_ELEMENT.getXmlLocalName(), INTRUMENTATION_ELEMENT); + elementDescs.put(PERMISSION_ELEMENT.getXmlLocalName(), PERMISSION_ELEMENT); + elementDescs.put(USES_PERMISSION_ELEMENT.getXmlLocalName(), USES_PERMISSION_ELEMENT); + elementDescs.put(USES_SDK_ELEMENT.getXmlLocalName(), USES_SDK_ELEMENT); + elementDescs.put(PERMISSION_GROUP_ELEMENT.getXmlLocalName(), PERMISSION_GROUP_ELEMENT); + elementDescs.put(PERMISSION_TREE_ELEMENT.getXmlLocalName(), PERMISSION_TREE_ELEMENT); + + // -- + + inflateElement(manifestMap, + overrides, + required, + elementDescs, + MANIFEST_ELEMENT, + "AndroidManifest"); //$NON-NLS-1$ + insertAttribute(MANIFEST_ELEMENT, PACKAGE_ATTR_DESC); + + XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor( + SdkConstants.ANDROID_NS_NAME, SdkConstants.ANDROID_URI); + insertAttribute(MANIFEST_ELEMENT, xmlns); + + /* + * + * + */ + assert sanityCheck(manifestMap, MANIFEST_ELEMENT); + } + + /** + * Sets up a mandatory attribute override using a ClassAttributeDescriptor + * with the specified class name. + * + * @param overrides The current map of overrides. + * @param elementName The element name to override, e.g. "application". + * If this name does NOT have a slash (/), the ANDROID_NAME_ATTR attribute will be overriden. + * Otherwise, if it contains a (/) the format is "element/attribute", for example + * "application/name" vs "application/backupAgent". + * @param className The fully qualified name of the base class of the attribute. + */ + private static void overrideClassName( + Map<String, ITextAttributeCreator> overrides, + String elementName, + final String className) { + overrideClassName(overrides, elementName, className, true); + } + + /** + * Sets up an attribute override using a ClassAttributeDescriptor + * with the specified class name. + * + * @param overrides The current map of overrides. + * @param elementName The element name to override, e.g. "application". + * If this name does NOT have a slash (/), the ANDROID_NAME_ATTR attribute will be overriden. + * Otherwise, if it contains a (/) the format is "element/attribute", for example + * "application/name" vs "application/backupAgent". + * @param className The fully qualified name of the base class of the attribute. + * @param mandatory True if this attribute is mandatory, false if optional. + */ + private static void overrideClassName( + Map<String, ITextAttributeCreator> overrides, + String elementName, + final String className, + final boolean mandatory) { + if (elementName.indexOf('/') == -1) { + elementName = elementName + '/' + ANDROID_NAME_ATTR; + } + overrides.put(elementName, + new ITextAttributeCreator() { + @Override + public TextAttributeDescriptor create(String xmlName, String nsUri, + IAttributeInfo attrInfo) { + if (attrInfo == null) { + attrInfo = new AttributeInfo(xmlName, Format.STRING_SET ); + } + + if (SdkConstants.CLASS_ACTIVITY.equals(className)) { + return new ClassAttributeDescriptor( + className, + PostActivityCreationAction.getAction(), + xmlName, + nsUri, + attrInfo, + mandatory, + true /*defaultToProjectOnly*/); + } else if (SdkConstants.CLASS_BROADCASTRECEIVER.equals(className)) { + return new ClassAttributeDescriptor( + className, + PostReceiverCreationAction.getAction(), + xmlName, + nsUri, + attrInfo, + mandatory, + true /*defaultToProjectOnly*/); + } else if (SdkConstants.CLASS_INSTRUMENTATION.equals(className)) { + return new ClassAttributeDescriptor( + className, + null, // no post action + xmlName, + nsUri, + attrInfo, + mandatory, + false /*defaultToProjectOnly*/); + } else { + return new ClassAttributeDescriptor( + className, + xmlName, + nsUri, + attrInfo, + mandatory); + } + } + }); + } + + /** + * Returns a new ElementDescriptor constructed from the information given here + * and the javadoc & attributes extracted from the style map if any. + * <p/> + * Creates an element with no attribute overrides. + */ + private ElementDescriptor createElement( + String xmlName, + ElementDescriptor[] childrenElements, + Mandatory mandatory) { + // Creates an element with no attribute overrides. + String styleName = guessStyleName(xmlName); + String sdkUrl = DescriptorsUtils.MANIFEST_SDK_URL + styleName; + String uiName = getUiName(xmlName); + + ElementDescriptor element = new ManifestElementDescriptor(xmlName, uiName, null, sdkUrl, + null, childrenElements, mandatory); + + return element; + } + + /** + * Returns a new ElementDescriptor constructed from its XML local name. + * <p/> + * This version creates an element not mandatory. + */ + private ElementDescriptor createElement(String xmlName) { + // Creates an element with no child and not mandatory + return createElement(xmlName, null, Mandatory.NOT_MANDATORY); + } + + /** + * Inserts an attribute in this element attribute list if it is not present there yet + * (based on the attribute XML name.) + * The attribute is inserted at the beginning of the attribute list. + */ + private void insertAttribute(ElementDescriptor element, AttributeDescriptor newAttr) { + AttributeDescriptor[] attributes = element.getAttributes(); + for (AttributeDescriptor attr : attributes) { + if (attr.getXmlLocalName().equals(newAttr.getXmlLocalName())) { + return; + } + } + + AttributeDescriptor[] newArray = new AttributeDescriptor[attributes.length + 1]; + newArray[0] = newAttr; + System.arraycopy(attributes, 0, newArray, 1, attributes.length); + element.setAttributes(newArray); + } + + /** + * "Inflates" the properties of an {@link ElementDescriptor} from the styleable declaration. + * <p/> + * This first creates all the attributes for the given ElementDescriptor. + * It then finds all children of the descriptor, inflates them recursively and sets them + * as child to this ElementDescriptor. + * + * @param styleMap The input styleable map for manifest elements & attributes. + * @param overrides A list of attribute overrides (to customize the type of the attribute + * descriptors). + * @param requiredAttributes Set of attributes to be marked as required. + * @param existingElementDescs A map of already created element descriptors, keyed by + * XML local name. This is used to use the static elements created initially by this + * class, which are referenced directly by editors (so that reloading an SDK won't + * break these references). + * @param elemDesc The current {@link ElementDescriptor} to inflate. + * @param styleName The name of the {@link ElementDescriptor} to inflate. Its XML local name + * will be guessed automatically from the style name. + */ + private void inflateElement( + Map<String, DeclareStyleableInfo> styleMap, + Map<String, ITextAttributeCreator> overrides, + Set<String> requiredAttributes, + HashMap<String, ElementDescriptor> existingElementDescs, + ElementDescriptor elemDesc, + String styleName) { + assert elemDesc != null; + assert styleName != null; + assert styleMap != null; + + if (styleMap == null) { + return; + } + + // define attributes + DeclareStyleableInfo style = styleMap.get(styleName); + if (style != null) { + ArrayList<AttributeDescriptor> attrDescs = new ArrayList<AttributeDescriptor>(); + DescriptorsUtils.appendAttributes(attrDescs, + elemDesc.getXmlLocalName(), + SdkConstants.NS_RESOURCES, + style.getAttributes(), + requiredAttributes, + overrides); + elemDesc.setTooltip(style.getJavaDoc()); + elemDesc.setAttributes(attrDescs.toArray(new AttributeDescriptor[attrDescs.size()])); + } + + // find all elements that have this one as parent + ArrayList<ElementDescriptor> children = new ArrayList<ElementDescriptor>(); + for (Entry<String, DeclareStyleableInfo> entry : styleMap.entrySet()) { + DeclareStyleableInfo childStyle = entry.getValue(); + boolean isParent = false; + String[] parents = childStyle.getParents(); + if (parents != null) { + for (String parent: parents) { + if (styleName.equals(parent)) { + isParent = true; + break; + } + } + } + if (isParent) { + String childStyleName = entry.getKey(); + String childXmlName = guessXmlName(childStyleName); + + // create or re-use element + ElementDescriptor child = existingElementDescs.get(childXmlName); + if (child == null) { + child = createElement(childXmlName); + existingElementDescs.put(childXmlName, child); + } + children.add(child); + + inflateElement(styleMap, + overrides, + requiredAttributes, + existingElementDescs, + child, + childStyleName); + } + } + elemDesc.setChildren(children.toArray(new ElementDescriptor[children.size()])); + } + + /** + * Get an UI name from the element XML name. + * <p/> + * Capitalizes the first letter and replace non-alphabet by a space followed by a capital. + */ + private static String getUiName(String xmlName) { + StringBuilder sb = new StringBuilder(); + + boolean capitalize = true; + for (char c : xmlName.toCharArray()) { + if (capitalize && c >= 'a' && c <= 'z') { + sb.append((char)(c + 'A' - 'a')); + capitalize = false; + } else if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) { + sb.append(' '); + capitalize = true; + } else { + sb.append(c); + } + } + + return sb.toString(); + } + + /** + * Guesses the style name for a given XML element name. + * <p/> + * The rules are: + * - capitalize the first letter: + * - if there's a dash, skip it and capitalize the next one + * - prefix AndroidManifest + * The exception is "manifest" which just becomes AndroidManifest. + * <p/> + * Examples: + * - manifest => AndroidManifest + * - application => AndroidManifestApplication + * - uses-permission => AndroidManifestUsesPermission + */ + private String guessStyleName(String xmlName) { + StringBuilder sb = new StringBuilder(); + + if (!xmlName.equals(MANIFEST_NODE_NAME)) { + boolean capitalize = true; + for (char c : xmlName.toCharArray()) { + if (capitalize && c >= 'a' && c <= 'z') { + sb.append((char)(c + 'A' - 'a')); + capitalize = false; + } else if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) { + // not a letter -- skip the character and capitalize the next one + capitalize = true; + } else { + sb.append(c); + } + } + } + + sb.insert(0, ANDROID_MANIFEST_STYLEABLE); + return sb.toString(); + } + + /** + * This method performs a sanity check to make sure all the styles declared in the + * manifestMap are actually defined in the actual element descriptors and reachable from + * the manifestElement root node. + */ + private boolean sanityCheck(Map<String, DeclareStyleableInfo> manifestMap, + ElementDescriptor manifestElement) { + TreeSet<String> elementsDeclared = new TreeSet<String>(); + findAllElementNames(manifestElement, elementsDeclared); + + TreeSet<String> stylesDeclared = new TreeSet<String>(); + for (String styleName : manifestMap.keySet()) { + if (styleName.startsWith(ANDROID_MANIFEST_STYLEABLE)) { + stylesDeclared.add(styleName); + } + } + + for (Iterator<String> it = elementsDeclared.iterator(); it.hasNext();) { + String xmlName = it.next(); + String styleName = guessStyleName(xmlName); + if (stylesDeclared.remove(styleName)) { + it.remove(); + } + } + + StringBuilder sb = new StringBuilder(); + if (!stylesDeclared.isEmpty()) { + sb.append("Warning, ADT/SDK Mismatch! The following elements are declared by the SDK but unknown to ADT: "); + for (String name : stylesDeclared) { + sb.append(guessXmlName(name)); + + if (!name.equals(stylesDeclared.last())) { + sb.append(", "); //$NON-NLS-1$ + } + } + + AdtPlugin.log(IStatus.WARNING, "%s", sb.toString()); + AdtPlugin.printToConsole((String)null, sb); + sb.setLength(0); + } + + if (!elementsDeclared.isEmpty()) { + sb.append("Warning, ADT/SDK Mismatch! The following elements are declared by ADT but not by the SDK: "); + for (String name : elementsDeclared) { + sb.append(name); + if (!name.equals(elementsDeclared.last())) { + sb.append(", "); //$NON-NLS-1$ + } + } + + AdtPlugin.log(IStatus.WARNING, "%s", sb.toString()); + AdtPlugin.printToConsole((String)null, sb); + } + + return true; + } + + /** + * Performs an approximate translation of the style name into a potential + * xml name. This is more or less the reverse from guessStyleName(). + * + * @return The XML local name for a given style name. + */ + private String guessXmlName(String name) { + StringBuilder sb = new StringBuilder(); + if (ANDROID_MANIFEST_STYLEABLE.equals(name)) { + sb.append(MANIFEST_NODE_NAME); + } else { + name = name.replace(ANDROID_MANIFEST_STYLEABLE, ""); //$NON-NLS-1$ + boolean first_char = true; + for (char c : name.toCharArray()) { + if (c >= 'A' && c <= 'Z') { + if (!first_char) { + sb.append('-'); + } + c = (char) (c - 'A' + 'a'); + } + sb.append(c); + first_char = false; + } + } + return sb.toString(); + } + + /** + * Helper method used by {@link #sanityCheck(Map, ElementDescriptor)} to find all the + * {@link ElementDescriptor} names defined by the tree of descriptors. + * <p/> + * Note: this assumes no circular reference in the tree of {@link ElementDescriptor}s. + */ + private void findAllElementNames(ElementDescriptor element, TreeSet<String> declared) { + declared.add(element.getXmlName()); + for (ElementDescriptor desc : element.getChildren()) { + findAllElementNames(desc, declared); + } + } + + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ClassAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ClassAttributeDescriptor.java new file mode 100644 index 000000000..1a27f8d00 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ClassAttributeDescriptor.java @@ -0,0 +1,106 @@ +/* + * 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.descriptors; + +import com.android.SdkConstants; +import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.manifest.model.UiClassAttributeNode; +import com.android.ide.eclipse.adt.internal.editors.manifest.model.UiClassAttributeNode.IPostTypeCreationAction; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; + +/** + * Describes an XML attribute representing a class name. + * It is displayed by a {@link UiClassAttributeNode}. + */ +public class ClassAttributeDescriptor extends TextAttributeDescriptor { + + /** Superclass of the class value. */ + private String mSuperClassName; + + private IPostTypeCreationAction mPostCreationAction; + + /** indicates if the class parameter is mandatory */ + boolean mMandatory; + + private final boolean mDefaultToProjectOnly; + + /** + * Creates a new {@link ClassAttributeDescriptor} + * @param superClassName the fully qualified name of the superclass of the class represented + * by the attribute. + * @param xmlLocalName The XML name of the attribute (case sensitive, with android: prefix). + * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. + * See {@link SdkConstants#NS_RESOURCES} for a common value. + * @param attrInfo The {@link IAttributeInfo} of this attribute. Can't be null. + * @param mandatory indicates if the class attribute is mandatory. + */ + public ClassAttributeDescriptor(String superClassName, + String xmlLocalName, + String nsUri, + IAttributeInfo attrInfo, + boolean mandatory) { + super(xmlLocalName, nsUri, attrInfo); + mSuperClassName = superClassName; + mDefaultToProjectOnly = true; + if (mandatory) { + mMandatory = true; + setRequired(true); + } + } + + /** + * Creates a new {@link ClassAttributeDescriptor} + * @param superClassName the fully qualified name of the superclass of the class represented + * by the attribute. + * @param postCreationAction the {@link IPostTypeCreationAction} to be executed on the + * newly created class. + * @param xmlLocalName The XML local name of the attribute (case sensitive). + * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. + * See {@link SdkConstants#NS_RESOURCES} for a common value. + * @param attrInfo The {@link IAttributeInfo} of this attribute. Can't be null. + * @param mandatory indicates if the class attribute is mandatory. + * @param defaultToProjectOnly True if only classes from the sources of this project should + * be shown by default in the class browser. + */ + public ClassAttributeDescriptor(String superClassName, + IPostTypeCreationAction postCreationAction, + String xmlLocalName, + String nsUri, + IAttributeInfo attrInfo, + boolean mandatory, + boolean defaultToProjectOnly) { + super(xmlLocalName, nsUri, attrInfo); + mSuperClassName = superClassName; + mPostCreationAction = postCreationAction; + mDefaultToProjectOnly = defaultToProjectOnly; + if (mandatory) { + mMandatory = true; + setRequired(true); + } + } + + /** + * @return A new {@link UiClassAttributeNode} linked to this descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiClassAttributeNode(mSuperClassName, mPostCreationAction, + mMandatory, this, uiParent, mDefaultToProjectOnly); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestElementDescriptor.java new file mode 100644 index 000000000..deb815e57 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestElementDescriptor.java @@ -0,0 +1,123 @@ +/* + * 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.descriptors; + +import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.manifest.model.UiManifestElementNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; + +/** + * {@link ManifestElementDescriptor} describes an XML element node, with its + * element name, its possible attributes, its possible child elements but also + * its display name and tooltip. + * + * This {@link ElementDescriptor} is specialized to create {@link UiManifestElementNode} UI nodes. + */ +public class ManifestElementDescriptor extends ElementDescriptor { + + /** + * Constructs a new {@link ManifestElementDescriptor}. + * + * @param xml_name The XML element node name. Case sensitive. + * @param ui_name The XML element name for the user interface, typically capitalized. + * @param tooltip An optional tooltip. Can be null or empty. + * @param sdk_url An optional SKD URL. Can be null or empty. + * @param attributes The list of allowed attributes. Can be null or empty. + * @param children The list of allowed children. Can be null or empty. + * @param mandatory Whether this node must always exist (even for empty models). + */ + public ManifestElementDescriptor(String xml_name, + String ui_name, + String tooltip, + String sdk_url, + AttributeDescriptor[] attributes, + ElementDescriptor[] children, + Mandatory mandatory) { + super(xml_name, ui_name, tooltip, sdk_url, attributes, children, mandatory); + } + + /** + * Constructs a new {@link ManifestElementDescriptor}. + * + * @param xml_name The XML element node name. Case sensitive. + * @param ui_name The XML element name for the user interface, typically capitalized. + * @param tooltip An optional tooltip. Can be null or empty. + * @param sdk_url An optional SKD URL. Can be null or empty. + * @param attributes The list of allowed attributes. Can be null or empty. + * @param children The list of allowed children. Can be null or empty. + * @param mandatory Whether this node must always exist (even for empty models). + */ + public ManifestElementDescriptor(String xml_name, + String ui_name, + String tooltip, + String sdk_url, + AttributeDescriptor[] attributes, + ElementDescriptor[] children, + boolean mandatory) { + super(xml_name, ui_name, tooltip, sdk_url, attributes, children, mandatory); + } + + /** + * Constructs a new {@link ManifestElementDescriptor}. + * + * @param xml_name The XML element node name. Case sensitive. + * @param ui_name The XML element name for the user interface, typically capitalized. + * @param tooltip An optional tooltip. Can be null or empty. + * @param sdk_url An optional SKD URL. Can be null or empty. + * @param attributes The list of allowed attributes. Can be null or empty. + * @param children The list of allowed children. Can be null or empty. + */ + public ManifestElementDescriptor(String xml_name, + String ui_name, + String tooltip, + String sdk_url, + AttributeDescriptor[] attributes, + ElementDescriptor[] children) { + super(xml_name, ui_name, tooltip, sdk_url, attributes, children, false); + } + + /** + * This is a shortcut for + * ManifestElementDescriptor(xml_name, xml_name.capitalize(), null, null, null, children). + * This constructor is mostly used for unit tests. + * + * @param xml_name The XML element node name. Case sensitive. + */ + public ManifestElementDescriptor(String xml_name, ElementDescriptor[] children) { + super(xml_name, children); + } + + /** + * This is a shortcut for + * ManifestElementDescriptor(xml_name, xml_name.capitalize(), null, null, null, null). + * This constructor is mostly used for unit tests. + * + * @param xml_name The XML element node name. Case sensitive. + */ + public ManifestElementDescriptor(String xml_name) { + super(xml_name, null); + } + + /** + * @return A new {@link UiElementNode} linked to this descriptor. + */ + @Override + public UiElementNode createUiNode() { + return new UiManifestElementNode(this); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestPkgAttrDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestPkgAttrDescriptor.java new file mode 100755 index 000000000..74b789487 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ManifestPkgAttrDescriptor.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2009 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.descriptors; + +import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ITextAttributeCreator; +import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.manifest.model.UiManifestPkgAttrNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; + +/** + * Describes a package XML attribute. It is displayed by a {@link UiManifestPkgAttrNode}. + * <p/> + * Used by the override for .../targetPackage in {@link AndroidManifestDescriptors}. + */ +public class ManifestPkgAttrDescriptor extends TextAttributeDescriptor { + + /** + * Used by {@link DescriptorsUtils} to create instances of this descriptor. + */ + public static final ITextAttributeCreator CREATOR = new ITextAttributeCreator() { + @Override + public TextAttributeDescriptor create(String xmlLocalName, + String nsUri, IAttributeInfo attrInfo) { + return new ManifestPkgAttrDescriptor(xmlLocalName, nsUri, attrInfo); + } + }; + + public ManifestPkgAttrDescriptor(String xmlLocalName, String nsUri, IAttributeInfo attrInfo) { + super(xmlLocalName, nsUri, attrInfo); + } + + /** + * @return A new {@link UiManifestPkgAttrNode} linked to this descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiManifestPkgAttrNode(this, uiParent); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PackageAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PackageAttributeDescriptor.java new file mode 100644 index 000000000..e8395ac40 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PackageAttributeDescriptor.java @@ -0,0 +1,41 @@ +/* + * 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.descriptors; + +import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.manifest.model.UiPackageAttributeNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; + +/** + * Describes a package XML attribute. It is displayed by a {@link UiPackageAttributeNode}. + */ +public class PackageAttributeDescriptor extends TextAttributeDescriptor { + + public PackageAttributeDescriptor(String xmlLocalName, String nsUri, IAttributeInfo attrInfo) { + super(xmlLocalName, nsUri, attrInfo); + } + + /** + * @return A new {@link UiPackageAttributeNode} linked to this descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiPackageAttributeNode(this, uiParent); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostActivityCreationAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostActivityCreationAction.java new file mode 100644 index 000000000..60c663dfc --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostActivityCreationAction.java @@ -0,0 +1,89 @@ +/* + * 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.descriptors; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.internal.editors.manifest.model.UiClassAttributeNode.IPostTypeCreationAction; + +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaModelException; + +/** + * Action to be executed after an Activity class is created. + */ +class PostActivityCreationAction implements IPostTypeCreationAction { + + private final static PostActivityCreationAction sAction = new PostActivityCreationAction(); + + private PostActivityCreationAction() { + // private constructor to enforce singleton. + } + + + /** + * Returns the action. + */ + public static IPostTypeCreationAction getAction() { + return sAction; + } + + /** + * Processes a newly created Activity. + * + */ + @Override + public void processNewType(IType newType) { + try { + String methodContent = + " /** Called when the activity is first created. */\n" + + " @Override\n" + + " public void onCreate(Bundle savedInstanceState) {\n" + + " super.onCreate(savedInstanceState);\n" + + "\n" + + " // TODO Auto-generated method stub\n" + + " }"; + newType.createMethod(methodContent, null /* sibling*/, false /* force */, + new NullProgressMonitor()); + + // we need to add the import for Bundle, so we need the compilation unit. + // Since the type could be enclosed in other types, we loop till we find it. + ICompilationUnit compilationUnit = null; + IJavaElement element = newType; + do { + IJavaElement parentElement = element.getParent(); + if (parentElement != null) { + if (parentElement.getElementType() == IJavaElement.COMPILATION_UNIT) { + compilationUnit = (ICompilationUnit)parentElement; + } + + element = parentElement; + } else { + break; + } + } while (compilationUnit == null); + + if (compilationUnit != null) { + compilationUnit.createImport(SdkConstants.CLASS_BUNDLE, + null /* sibling */, new NullProgressMonitor()); + } + } catch (JavaModelException e) { + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostReceiverCreationAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostReceiverCreationAction.java new file mode 100644 index 000000000..8b2c36144 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/PostReceiverCreationAction.java @@ -0,0 +1,89 @@ +/* + * 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.descriptors; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.internal.editors.manifest.model.UiClassAttributeNode.IPostTypeCreationAction; + +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaModelException; + +/** + * Action to be executed after an BroadcastReceiver class is created. + */ +class PostReceiverCreationAction implements IPostTypeCreationAction { + + private final static PostReceiverCreationAction sAction = new PostReceiverCreationAction(); + + private PostReceiverCreationAction() { + // private constructor to enforce singleton. + } + + /** + * Returns the action. + */ + public static IPostTypeCreationAction getAction() { + return sAction; + } + + /** + * Processes a newly created Activity. + * + */ + @Override + public void processNewType(IType newType) { + try { + String methodContent = + " @Override\n" + + " public void onReceive(Context context, Intent intent) {\n" + + " // TODO Auto-generated method stub\n" + + " }"; + newType.createMethod(methodContent, null /* sibling*/, false /* force */, + new NullProgressMonitor()); + + // we need to add the import for Bundle, so we need the compilation unit. + // Since the type could be enclosed in other types, we loop till we find it. + ICompilationUnit compilationUnit = null; + IJavaElement element = newType; + do { + IJavaElement parentElement = element.getParent(); + if (parentElement != null) { + if (parentElement.getElementType() == IJavaElement.COMPILATION_UNIT) { + compilationUnit = (ICompilationUnit)parentElement; + } + + element = parentElement; + } else { + break; + } + } while (compilationUnit == null); + + if (compilationUnit != null) { + compilationUnit.createImport(SdkConstants.CLASS_CONTEXT, + null /* sibling */, new NullProgressMonitor()); + compilationUnit.createImport(SdkConstants.CLASS_INTENT, + null /* sibling */, new NullProgressMonitor()); + } + } catch (JavaModelException e) { + // looks like the class already existed (this happens when the user check to create + // inherited abstract methods). + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ThemeAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ThemeAttributeDescriptor.java new file mode 100644 index 000000000..881d75361 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/descriptors/ThemeAttributeDescriptor.java @@ -0,0 +1,57 @@ +/* + * 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.descriptors; + +import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ITextAttributeCreator; +import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiResourceAttributeNode; +import com.android.resources.ResourceType; + +/** + * Describes a Theme/Style XML attribute displayed by a {@link UiResourceAttributeNode} + * <p/> + * Used by the override for .../theme in {@link AndroidManifestDescriptors}. + */ +public final class ThemeAttributeDescriptor extends TextAttributeDescriptor { + + /** + * Used by {@link DescriptorsUtils} to create instances of this descriptor. + */ + public static final ITextAttributeCreator CREATOR = new ITextAttributeCreator() { + @Override + public TextAttributeDescriptor create(String xmlLocalName, + String nsUri, IAttributeInfo attrInfo) { + return new ThemeAttributeDescriptor(xmlLocalName, nsUri, attrInfo); + } + }; + + public ThemeAttributeDescriptor(String xmlLocalName, String nsUri, IAttributeInfo attrInfo) { + super(xmlLocalName, nsUri, attrInfo); + } + + /** + * @return A new {@link UiResourceAttributeNode} linked to this theme descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiResourceAttributeNode(ResourceType.STYLE, this, uiParent); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiClassAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiClassAttributeNode.java new file mode 100644 index 000000000..4c829d9ec --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiClassAttributeNode.java @@ -0,0 +1,736 @@ +/* + * 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; + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestElementNode.java new file mode 100644 index 000000000..0151d4d46 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestElementNode.java @@ -0,0 +1,132 @@ +/* + * 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.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.ManifestElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; + +import org.w3c.dom.Element; + +/** + * Represents an XML node that can be modified by the user interface in the XML editor. + * <p/> + * Each tree viewer used in the application page's parts needs to keep a model representing + * each underlying node in the tree. This interface represents the base type for such a node. + * <p/> + * Each node acts as an intermediary model between the actual XML model (the real data support) + * and the tree viewers or the corresponding page parts. + * <p/> + * Element nodes don't contain data per se. Their data is contained in their attributes + * as well as their children's attributes, see {@link UiAttributeNode}. + * <p/> + * The structure of a given {@link UiElementNode} is declared by a corresponding + * {@link ElementDescriptor}. + */ +public final class UiManifestElementNode extends UiElementNode { + + /** + * Creates a new {@link UiElementNode} described by a given {@link ElementDescriptor}. + * + * @param elementDescriptor The {@link ElementDescriptor} for the XML node. Cannot be null. + */ + public UiManifestElementNode(ManifestElementDescriptor elementDescriptor) { + super(elementDescriptor); + } + + /** + * Computes a short string describing the UI node suitable for tree views. + * Uses the element's attribute "android:name" if present, or the "android:label" one + * followed by the element's name if not repeated. + * + * @return A short string describing the UI node suitable for tree views. + */ + @Override + public String getShortDescription() { + AndroidTargetData target = getAndroidTarget(); + AndroidManifestDescriptors manifestDescriptors = null; + if (target != null) { + manifestDescriptors = target.getManifestDescriptors(); + } + + String name = getDescriptor().getUiName(); + + if (manifestDescriptors != null && + getXmlNode() != null && + getXmlNode() instanceof Element && + getXmlNode().hasAttributes()) { + + // Application and Manifest nodes have a special treatment: they are unique nodes + // so we don't bother trying to differentiate their strings and we fall back to + // just using the UI name below. + ElementDescriptor desc = getDescriptor(); + if (desc != manifestDescriptors.getManifestElement() && + desc != manifestDescriptors.getApplicationElement()) { + Element elem = (Element) getXmlNode(); + String attr = _Element_getAttributeNS(elem, + SdkConstants.NS_RESOURCES, + AndroidManifestDescriptors.ANDROID_NAME_ATTR); + if (attr == null || attr.length() == 0) { + attr = _Element_getAttributeNS(elem, + SdkConstants.NS_RESOURCES, + AndroidManifestDescriptors.ANDROID_LABEL_ATTR); + } + if (attr != null && attr.length() > 0) { + // If the ui name is repeated in the attribute value, don't use it. + // Typical case is to avoid ".pkg.MyActivity (Activity)". + if (attr.contains(name)) { + return attr; + } else { + return String.format("%1$s (%2$s)", attr, name); + } + } + } + } + + return String.format("%1$s", name); + } + + /** + * Retrieves an attribute value by local name and namespace URI. + * <br>Per [<a href='http://www.w3.org/TR/1999/REC-xml-names-19990114/'>XML Namespaces</a>] + * , applications must use the value <code>null</code> as the + * <code>namespaceURI</code> parameter for methods if they wish to have + * no namespace. + * <p/> + * Note: This is a wrapper around {@link Element#getAttributeNS(String, String)}. + * In some versions of webtools, the getAttributeNS implementation crashes with an NPE. + * This wrapper will return null instead. + * + * @see Element#getAttributeNS(String, String) + * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108">https://bugs.eclipse.org/bugs/show_bug.cgi?id=318108</a> + * @return The result from {@link Element#getAttributeNS(String, String)} or or an empty string. + */ + private String _Element_getAttributeNS(Element element, + String namespaceURI, + String localName) { + try { + return element.getAttributeNS(namespaceURI, localName); + } catch (Exception ignore) { + return ""; + } + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestPkgAttrNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestPkgAttrNode.java new file mode 100755 index 000000000..60d9125f6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiManifestPkgAttrNode.java @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2009 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.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.AdtPlugin; +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.ManifestEditor; +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.AndroidManifestHelper; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.wizards.actions.NewProjectAction; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizard; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ILabelProviderListener; +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.graphics.Image; +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.Text; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.dialogs.ElementListSelectionDialog; +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.eclipse.ui.part.FileEditorInput; + +import java.util.TreeSet; + +/** + * Represents an XML attribute to select an existing manifest package, that can be modified using + * a simple text field or a dialog to choose an existing package. + * <p/> + * See {@link UiTextAttributeNode} for more information. + */ +public class UiManifestPkgAttrNode extends UiTextAttributeNode { + + /** + * Creates a {@link UiManifestPkgAttrNode} object that will display ui to select or create + * a manifest package. + * @param attributeDescriptor the {@link AttributeDescriptor} object linked to the Ui Node. + */ + public UiManifestPkgAttrNode(AttributeDescriptor attributeDescriptor, UiElementNode uiParent) { + super(attributeDescriptor, uiParent); + } + + /* (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, final IManagedForm managedForm) { + setManagedForm(managedForm); + FormToolkit toolkit = managedForm.getToolkit(); + TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor(); + + StringBuilder label = new StringBuilder(); + label.append("<form><p><a href='unused'>"); //$NON-NLS-1$ + label.append(desc.getUiName()); + label.append("</a></p></form>"); //$NON-NLS-1$ + 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); + doLabelClick(); + } + }); + 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); + + setTextWidget(text); + + Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH); + + browseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + doBrowseClick(); + } + }); + + } + + /* (non-java doc) + * Adds a validator to the text field that calls managedForm.getMessageManager(). + */ + @Override + protected void onAddValidators(final Text text) { + ModifyListener listener = new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + String package_name = text.getText(); + if (package_name.indexOf('.') < 1) { + getManagedForm().getMessageManager().addMessage(text, + "Package name should contain at least two identifiers.", + null /* data */, IMessageProvider.ERROR, text); + } else { + getManagedForm().getMessageManager().removeMessage(text, 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) { + getManagedForm().getMessageManager().removeMessage(text, text); + } + }); + + // Finally call the validator once to make sure the initial value is processed + listener.modifyText(null); + } + + /** + * Handles response to the Browse button by creating a Package dialog. + * */ + private void doBrowseClick() { + + // Display the list of AndroidManifest packages in a selection dialog + ElementListSelectionDialog dialog = new ElementListSelectionDialog( + getTextWidget().getShell(), + new ILabelProvider() { + @Override + public Image getImage(Object element) { + return null; + } + + @Override + public String getText(Object element) { + return element.toString(); + } + + @Override + public void addListener(ILabelProviderListener listener) { + } + + @Override + public void dispose() { + } + + @Override + public boolean isLabelProperty(Object element, String property) { + return false; + } + + @Override + public void removeListener(ILabelProviderListener listener) { + } + }); + + dialog.setTitle("Android Manifest Package Selection"); + dialog.setMessage("Select the Android Manifest package to target."); + + dialog.setElements(getPossibleValues(null)); + + // open the dialog and use the object selected if OK was clicked, or null otherwise + if (dialog.open() == Window.OK) { + String result = (String) dialog.getFirstResult(); + if (result != null && result.length() > 0) { + getTextWidget().setText(result); + } + } + } + + /** + * Handles response to the Label hyper link being activated. + */ + private void doLabelClick() { + // get the current package name + String package_name = getTextWidget().getText().trim(); + + if (package_name.length() == 0) { + createNewProject(); + } else { + displayExistingManifest(package_name); + } + } + + /** + * When the label is clicked and there's already a package name, this method + * attempts to find the project matching the android package name and it attempts + * to open the manifest editor. + * + * @param package_name The android package name to find. Must not be null. + */ + private void displayExistingManifest(String package_name) { + + // Look for the first project that uses this package name + for (IJavaProject project : BaseProjectHelper.getAndroidProjects(null /*filter*/)) { + // check that there is indeed a manifest file. + IFile manifestFile = ProjectHelper.getManifest(project.getProject()); + if (manifestFile == null) { + // no file? skip this project. + continue; + } + + ManifestData manifestData = AndroidManifestHelper.parseForData(manifestFile); + if (manifestData == null) { + // skip this project. + continue; + } + + if (package_name.equals(manifestData.getPackage())) { + // Found the project. + + IWorkbenchWindow win = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (win != null) { + IWorkbenchPage page = win.getActivePage(); + if (page != null) { + try { + page.openEditor( + new FileEditorInput(manifestFile), + ManifestEditor.ID, + true, /* activate */ + IWorkbenchPage.MATCH_INPUT); + } catch (PartInitException e) { + AdtPlugin.log(e, + "Opening editor failed for %s", //$NON-NLS-1$ + manifestFile.getFullPath()); + } + } + } + + // We found the project; even if we failed there's no need to keep looking. + return; + } + } + } + + /** + * Displays the New Project Wizard to create a new project. + * If one is successfully created, use the Android Package name. + */ + private void createNewProject() { + + NewProjectAction npwAction = new NewProjectAction(); + npwAction.run(null /*action*/); + if (npwAction.getDialogResult() == Dialog.OK) { + NewProjectWizard npw = (NewProjectWizard) npwAction.getWizard(); + String name = npw.getPackageName(); + if (name != null && name.length() > 0) { + getTextWidget().setText(name); + } + } + } + + /** + * Returns all the possible android package names that could be used. + * The prefix is not used. + * + * {@inheritDoc} + */ + @Override + public String[] getPossibleValues(String prefix) { + TreeSet<String> packages = new TreeSet<String>(); + + for (IJavaProject project : BaseProjectHelper.getAndroidProjects(null /*filter*/)) { + // check that there is indeed a manifest file. + ManifestData manifestData = AndroidManifestHelper.parseForData(project.getProject()); + if (manifestData == null) { + // skip this project. + continue; + } + + packages.add(manifestData.getPackage()); + } + + return packages.toArray(new String[packages.size()]); + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiPackageAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiPackageAttributeNode.java new file mode 100644 index 000000000..e6a2007b3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiPackageAttributeNode.java @@ -0,0 +1,321 @@ +/* + * 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.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.ui.SectionHelper; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiTextAttributeNode; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IStatus; +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.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.ui.JavaUI; +import org.eclipse.jdt.ui.actions.OpenNewPackageWizardAction; +import org.eclipse.jdt.ui.actions.ShowInPackageViewAction; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.viewers.StructuredSelection; +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.Text; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.IWorkbenchPartSite; +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 java.util.ArrayList; + +/** + * Represents an XML attribute for a package, that can be modified using a simple text field or + * a dialog to choose an existing package. Also, there's a link to create a new package. + * <p/> + * See {@link UiTextAttributeNode} for more information. + */ +public class UiPackageAttributeNode extends UiTextAttributeNode { + + /** + * Creates a {@link UiPackageAttributeNode} object that will display ui to select or create + * a package. + * @param attributeDescriptor the {@link AttributeDescriptor} object linked to the Ui Node. + */ + public UiPackageAttributeNode(AttributeDescriptor attributeDescriptor, UiElementNode uiParent) { + super(attributeDescriptor, uiParent); + } + + /* (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, final IManagedForm managedForm) { + setManagedForm(managedForm); + FormToolkit toolkit = managedForm.getToolkit(); + TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor(); + + StringBuilder label = new StringBuilder(); + label.append("<form><p><a href='unused'>"); //$NON-NLS-1$ + label.append(desc.getUiName()); + label.append("</a></p></form>"); //$NON-NLS-1$ + 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); + doLabelClick(); + } + }); + 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); + + setTextWidget(text); + + Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH); + + browseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + doBrowseClick(); + } + }); + + } + + /* (non-java doc) + * Adds a validator to the text field that calls managedForm.getMessageManager(). + */ + @Override + protected void onAddValidators(final Text text) { + ModifyListener listener = new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + String package_name = text.getText(); + if (package_name.indexOf('.') < 1) { + getManagedForm().getMessageManager().addMessage(text, + "Package name should contain at least two identifiers.", + null /* data */, IMessageProvider.ERROR, text); + } else { + getManagedForm().getMessageManager().removeMessage(text, 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) { + getManagedForm().getMessageManager().removeMessage(text, text); + } + }); + + // Finally call the validator once to make sure the initial value is processed + listener.modifyText(null); + } + + /** + * Handles response to the Browse button by creating a Package dialog. + * */ + private void doBrowseClick() { + Text text = getTextWidget(); + + // we need to get the project of the manifest. + IProject project = getProject(); + if (project != null) { + + try { + SelectionDialog dlg = JavaUI.createPackageDialog(text.getShell(), + JavaCore.create(project), 0); + dlg.setTitle("Select Android Package"); + dlg.setMessage("Select the package for the Android project."); + SelectionDialog.setDefaultImage(AdtPlugin.getAndroidLogo()); + + if (dlg.open() == Window.OK) { + Object[] results = dlg.getResult(); + if (results.length == 1) { + setPackageTextField((IPackageFragment)results[0]); + } + } + } catch (JavaModelException e1) { + } + } + } + + /** + * Handles response to the Label hyper link being activated. + */ + private void doLabelClick() { + // get the current package name + String package_name = getTextWidget().getText().trim(); + + if (package_name.length() == 0) { + createNewPackage(); + } else { + // Try to select the package in the Package Explorer for the current + // project and the current editor's site. + + IProject project = getProject(); + if (project == null) { + AdtPlugin.log(IStatus.ERROR, "Failed to get project for UiPackageAttribute"); //$NON-NLS-1$ + return; + } + + IWorkbenchPartSite site = getUiParent().getEditor().getSite(); + if (site == null) { + AdtPlugin.log(IStatus.ERROR, "Failed to get editor site for UiPackageAttribute"); //$NON-NLS-1$ + return; + } + + for (IPackageFragmentRoot root : getPackageFragmentRoots(project)) { + IPackageFragment fragment = root.getPackageFragment(package_name); + if (fragment != null && fragment.exists()) { + ShowInPackageViewAction action = new ShowInPackageViewAction(site); + action.run(fragment); + // This action's run() doesn't provide the status (although internally it could) + // so we just assume it worked. + return; + } + } + } + } + + /** + * Utility method that returns the project for the current file being edited. + * + * @return The IProject for the current file being edited or null. + */ + 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; + } + + /** + * Utility method that computes and returns the list of {@link IPackageFragmentRoot} + * corresponding to the source folder of the specified project. + * + * @param project the project + * @return an array of IPackageFragmentRoot. Can be empty but not null. + */ + private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project) { + 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) { + result.add(roots[i]); + } + } + } catch (JavaModelException e) { + } + + return result.toArray(new IPackageFragmentRoot[result.size()]); + } + + /** + * Utility method that sets the package's text field to the package fragment's name. + * */ + private void setPackageTextField(IPackageFragment type) { + Text text = getTextWidget(); + + String name = type.getElementName(); + + text.setText(name); + } + + + /** + * Displays and handles a "Create Package Wizard". + * + * This is invoked by doLabelClick() when clicking on the hyperlink label with an + * empty package text field. + */ + private void createNewPackage() { + OpenNewPackageWizardAction action = new OpenNewPackageWizardAction(); + + IProject project = getProject(); + action.setSelection(new StructuredSelection(project)); + action.run(); + + IJavaElement element = action.getCreatedElement(); + if (element != null && + element.exists() && + element.getElementType() == IJavaElement.PACKAGE_FRAGMENT) { + setPackageTextField((IPackageFragment) element); + } + } + + @Override + public String[] getPossibleValues(String prefix) { + // TODO: compute a list of existing packages for content assist completion + return null; + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationAttributesPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationAttributesPart.java new file mode 100644 index 000000000..7d3f6a89f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationAttributesPart.java @@ -0,0 +1,175 @@ +/* + * 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.pages; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.adt.internal.editors.ui.UiElementPart; +import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; + +/** + * Application's attributes section part for Application page. + * <p/> + * This part is displayed at the top of the application page and displays all the possible + * attributes of an application node in the AndroidManifest (icon, class name, label, etc.) + */ +final class ApplicationAttributesPart extends UiElementPart { + + /** Listen to changes to the UI node for <application> and updates the UI */ + private AppNodeUpdateListener mAppNodeUpdateListener; + /** ManagedForm needed to create the UI controls */ + private IManagedForm mManagedForm; + + public ApplicationAttributesPart(Composite body, FormToolkit toolkit, ManifestEditor editor, + UiElementNode applicationUiNode) { + super(body, toolkit, editor, applicationUiNode, + "Application Attributes", // section title + "Defines the attributes specific to the application.", // section description + Section.TWISTIE | Section.EXPANDED); + } + + /** + * Changes and refreshes the Application UI node handle by the this part. + */ + @Override + public void setUiElementNode(UiElementNode uiElementNode) { + super.setUiElementNode(uiElementNode); + + createUiAttributes(mManagedForm); + } + + /* (non-java doc) + * Create the controls to edit the attributes for the given ElementDescriptor. + * <p/> + * This MUST not be called by the constructor. Instead it must be called from + * <code>initialize</code> (i.e. right after the form part is added to the managed form.) + * <p/> + * Derived classes can override this if necessary. + * + * @param managedForm The owner managed form + */ + @Override + protected void createFormControls(final IManagedForm managedForm) { + mManagedForm = managedForm; + setTable(createTableLayout(managedForm.getToolkit(), 4 /* numColumns */)); + + mAppNodeUpdateListener = new AppNodeUpdateListener(); + getUiElementNode().addUpdateListener(mAppNodeUpdateListener); + + createUiAttributes(mManagedForm); + } + + @Override + public void dispose() { + super.dispose(); + if (getUiElementNode() != null && mAppNodeUpdateListener != null) { + getUiElementNode().removeUpdateListener(mAppNodeUpdateListener); + mAppNodeUpdateListener = null; + } + } + + @Override + protected void createUiAttributes(IManagedForm managedForm) { + Composite table = getTable(); + if (table == null || managedForm == null) { + return; + } + + // Remove any old UI controls + for (Control c : table.getChildren()) { + c.dispose(); + } + + UiElementNode uiElementNode = getUiElementNode(); + AttributeDescriptor[] attr_desc_list = uiElementNode.getAttributeDescriptors(); + + // Display the attributes in 2 columns: + // attr 0 | attr 4 + // attr 1 | attr 5 + // attr 2 | attr 6 + // attr 3 | attr 7 + // that is we have to fill the grid in order 0, 4, 1, 5, 2, 6, 3, 7 + // thus index = i/2 + (i is odd * n/2) + int n = attr_desc_list.length; + int n2 = (int) Math.ceil(n / 2.0); + for (int i = 0; i < n; i++) { + AttributeDescriptor attr_desc = attr_desc_list[i / 2 + (i & 1) * n2]; + if (attr_desc instanceof XmlnsAttributeDescriptor) { + // Do not show hidden attributes + continue; + } + + UiAttributeNode ui_attr = uiElementNode.findUiAttribute(attr_desc); + if (ui_attr != null) { + ui_attr.createUiControl(table, managedForm); + } else { + // The XML has an extra attribute which wasn't declared in + // AndroidManifestDescriptors. This is not a problem, we just ignore it. + AdtPlugin.log(IStatus.WARNING, + "Attribute %1$s not declared in node %2$s, ignored.", //$NON-NLS-1$ + attr_desc.getXmlLocalName(), + uiElementNode.getDescriptor().getXmlName()); + } + } + + if (n == 0) { + createLabel(table, managedForm.getToolkit(), + "No attributes to display, waiting for SDK to finish loading...", + null /* tooltip */ ); + } + + // Initialize the enabled/disabled state + if (mAppNodeUpdateListener != null) { + mAppNodeUpdateListener.uiElementNodeUpdated(uiElementNode, null /* state, not used */); + } + + // Tell the section that the layout has changed. + layoutChanged(); + } + + /** + * This listener synchronizes the UI with the actual presence of the application XML node. + */ + private class AppNodeUpdateListener implements IUiUpdateListener { + @Override + public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) { + // The UiElementNode for the application XML node always exists, even + // if there is no corresponding XML node in the XML file. + // + // We enable the UI here if the XML node is not null. + Composite table = getTable(); + boolean exists = (ui_node.getXmlNode() != null); + if (table != null && table.getEnabled() != exists) { + table.setEnabled(exists); + for (Control c : table.getChildren()) { + c.setEnabled(exists); + } + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationPage.java new file mode 100644 index 000000000..06a3d3f3e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationPage.java @@ -0,0 +1,136 @@ +/* + * 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.pages; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.adt.internal.editors.ui.tree.UiTreeBlock; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.editor.FormPage; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.ScrolledForm; + + +/** + * Page for "Application" settings, part of the AndroidManifest form editor. + * <p/> + * Useful reference: + * <a href="http://www.eclipse.org/articles/Article-Forms/article.html"> + * http://www.eclipse.org/articles/Article-Forms/article.html</a> + */ +public final class ApplicationPage extends FormPage implements IPageImageProvider { + /** Page id used for switching tabs programmatically */ + public final static String PAGE_ID = "application_page"; //$NON-NLS-1$ + + /** Container editor */ + ManifestEditor mEditor; + /** The Application Toogle part */ + private ApplicationToggle mTooglePart; + /** The Application Attributes part */ + private ApplicationAttributesPart mAttrPart; + /** The tree view block */ + private UiTreeBlock mTreeBlock; + + public ApplicationPage(ManifestEditor editor) { + super(editor, PAGE_ID, "Application"); // tab's label, keep it short + mEditor = editor; + } + + @Override + public Image getPageImage() { + return IconFactory.getInstance().getIcon(getTitle(), + IconFactory.COLOR_BLUE, + IconFactory.SHAPE_RECT); + } + + /** + * Creates the content in the form hosted in this page. + * + * @param managedForm the form hosted in this page. + */ + @Override + protected void createFormContent(IManagedForm managedForm) { + super.createFormContent(managedForm); + ScrolledForm form = managedForm.getForm(); + form.setText("Android Manifest Application"); + form.setImage(AdtPlugin.getAndroidLogo()); + + UiElementNode appUiNode = getUiApplicationNode(); + + Composite body = form.getBody(); + FormToolkit toolkit = managedForm.getToolkit(); + + // We usually prefer to have a ColumnLayout here. However + // MasterDetailsBlock.createContent() below will reset the body's layout to a grid layout. + mTooglePart = new ApplicationToggle(body, toolkit, mEditor, appUiNode); + mTooglePart.getSection().setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + managedForm.addPart(mTooglePart); + mAttrPart = new ApplicationAttributesPart(body, toolkit, mEditor, appUiNode); + mAttrPart.getSection().setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + managedForm.addPart(mAttrPart); + + mTreeBlock = new UiTreeBlock(mEditor, appUiNode, + false /* autoCreateRoot */, + null /* element filters */, + "Application Nodes", + "List of all elements in the application"); + mTreeBlock.createContent(managedForm); + } + + /** + * Retrieves the application UI node. Since this is a mandatory node, it *always* + * exists, even if there is no matching XML node. + */ + private UiElementNode getUiApplicationNode() { + AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors(); + if (manifestDescriptor != null) { + ElementDescriptor desc = manifestDescriptor.getApplicationElement(); + return mEditor.getUiRootNode().findUiChildNode(desc.getXmlName()); + } else { + // return the ui root node, as a dummy application root node. + return mEditor.getUiRootNode(); + } + } + + /** + * Changes and refreshes the Application UI node handled by the sub parts. + */ + public void refreshUiApplicationNode() { + UiElementNode appUiNode = getUiApplicationNode(); + if (mTooglePart != null) { + mTooglePart.setUiElementNode(appUiNode); + } + if (mAttrPart != null) { + mAttrPart.setUiElementNode(appUiNode); + } + if (mTreeBlock != null) { + mTreeBlock.changeRootAndDescriptors(appUiNode, + null /* element filters */, + true /* refresh */); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationToggle.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationToggle.java new file mode 100644 index 000000000..159f08959 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/ApplicationToggle.java @@ -0,0 +1,312 @@ +/* + * 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.pages; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.adt.internal.editors.ui.UiElementPart; +import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener; +import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener.UiUpdateState; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.utils.SdkUtils; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.widgets.FormText; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; +import org.eclipse.ui.forms.widgets.TableWrapData; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.Text; + +/** + * Appllication Toogle section part for application page. + */ +final class ApplicationToggle extends UiElementPart { + + /** Checkbox indicating whether an application node is present */ + private Button mCheckbox; + /** Listen to changes to the UI node for <application> and updates the checkbox */ + private AppNodeUpdateListener mAppNodeUpdateListener; + /** Internal flag to know where we're programmatically modifying the checkbox and we want to + * avoid triggering the checkbox's callback. */ + public boolean mInternalModification; + private FormText mTooltipFormText; + + public ApplicationToggle(Composite body, FormToolkit toolkit, ManifestEditor editor, + UiElementNode applicationUiNode) { + super(body, toolkit, editor, applicationUiNode, + "Application Toggle", + null, /* description */ + Section.TWISTIE | Section.EXPANDED); + } + + @Override + public void dispose() { + super.dispose(); + if (getUiElementNode() != null && mAppNodeUpdateListener != null) { + getUiElementNode().removeUpdateListener(mAppNodeUpdateListener); + mAppNodeUpdateListener = null; + } + } + + /** + * Changes and refreshes the Application UI node handle by the this part. + */ + @Override + public void setUiElementNode(UiElementNode uiElementNode) { + super.setUiElementNode(uiElementNode); + + updateTooltip(); + + // Set the state of the checkbox + mAppNodeUpdateListener.uiElementNodeUpdated(getUiElementNode(), + UiUpdateState.CHILDREN_CHANGED); + } + + /** + * Create the controls to edit the attributes for the given ElementDescriptor. + * <p/> + * This MUST not be called by the constructor. Instead it must be called from + * <code>initialize</code> (i.e. right after the form part is added to the managed form.) + * + * @param managedForm The owner managed form + */ + @Override + protected void createFormControls(IManagedForm managedForm) { + FormToolkit toolkit = managedForm.getToolkit(); + Composite table = createTableLayout(toolkit, 1 /* numColumns */); + + mTooltipFormText = createFormText(table, toolkit, true, "<form></form>", + false /* setupLayoutData */); + updateTooltip(); + + mCheckbox = toolkit.createButton(table, + "Define an <application> tag in the AndroidManifest.xml", + SWT.CHECK); + mCheckbox.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.TOP)); + mCheckbox.setSelection(false); + mCheckbox.addSelectionListener(new CheckboxSelectionListener()); + + mAppNodeUpdateListener = new AppNodeUpdateListener(); + getUiElementNode().addUpdateListener(mAppNodeUpdateListener); + + // Initialize the state of the checkbox + mAppNodeUpdateListener.uiElementNodeUpdated(getUiElementNode(), + UiUpdateState.CHILDREN_CHANGED); + + // Tell the section that the layout has changed. + layoutChanged(); + } + + /** + * Updates the application tooltip in the form text. + * If there is no tooltip, the form text is hidden. + */ + private void updateTooltip() { + boolean isVisible = false; + + String tooltip = getUiElementNode().getDescriptor().getTooltip(); + if (tooltip != null) { + tooltip = DescriptorsUtils.formatFormText(tooltip, + getUiElementNode().getDescriptor(), + Sdk.getCurrent().getDocumentationBaseUrl()); + + mTooltipFormText.setText(tooltip, true /* parseTags */, true /* expandURLs */); + mTooltipFormText.setImage(DescriptorsUtils.IMAGE_KEY, AdtPlugin.getAndroidLogo()); + mTooltipFormText.addHyperlinkListener(getEditor().createHyperlinkListener()); + isVisible = true; + } + + mTooltipFormText.setVisible(isVisible); + } + + /** + * This listener synchronizes the XML application node when the checkbox + * is changed by the user. + */ + private class CheckboxSelectionListener extends SelectionAdapter { + private Node mUndoXmlNode; + private Node mUndoXmlParent; + private Node mUndoXmlNextNode; + private Node mUndoXmlNextElement; + private Document mUndoXmlDocument; + + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + if (!mInternalModification && getUiElementNode() != null) { + getUiElementNode().getEditor().wrapUndoEditXmlModel( + mCheckbox.getSelection() + ? "Create or restore Application node" + : "Remove Application node", + new Runnable() { + @Override + public void run() { + if (mCheckbox.getSelection()) { + // The user wants an <application> node. + // Either restore a previous one + // or create a full new one. + boolean create = true; + if (mUndoXmlNode != null) { + create = !restoreApplicationNode(); + } + if (create) { + getUiElementNode().createXmlNode(); + } + } else { + // Users no longer wants the <application> node. + removeApplicationNode(); + } + } + }); + } + } + + /** + * Restore a previously "saved" application node. + * + * @return True if the node could be restored, false otherwise. + */ + private boolean restoreApplicationNode() { + if (mUndoXmlDocument == null || mUndoXmlNode == null) { + return false; + } + + // Validate node references... + mUndoXmlParent = validateNode(mUndoXmlDocument, mUndoXmlParent); + mUndoXmlNextNode = validateNode(mUndoXmlDocument, mUndoXmlNextNode); + mUndoXmlNextElement = validateNode(mUndoXmlDocument, mUndoXmlNextElement); + + if (mUndoXmlParent == null){ + // If the parent node doesn't exist, try to find a new manifest node. + // If it doesn't exist, create it. + mUndoXmlParent = getUiElementNode().getUiParent().prepareCommit(); + mUndoXmlNextNode = null; + mUndoXmlNextElement = null; + } + + boolean success = false; + if (mUndoXmlParent != null) { + // If the parent is still around, reuse the same node. + + // Ideally we want to insert the node before what used to be its next sibling. + // If that's not possible, we try to insert it before its next sibling element. + // If that's not possible either, it will be inserted at the end of the parent's. + Node next = mUndoXmlNextNode; + if (next == null) { + next = mUndoXmlNextElement; + } + mUndoXmlParent.insertBefore(mUndoXmlNode, next); + if (next == null) { + Text sep = mUndoXmlDocument.createTextNode(SdkUtils.getLineSeparator()); + mUndoXmlParent.insertBefore(sep, null); // insert separator before end tag + } + success = true; + } + + // Remove internal references to avoid using them twice + mUndoXmlParent = null; + mUndoXmlNextNode = null; + mUndoXmlNextElement = null; + mUndoXmlNode = null; + mUndoXmlDocument = null; + return success; + } + + /** + * Validates that the given xml_node is still either the root node or one of its + * direct descendants. + * + * @param root_node The root of the node hierarchy to examine. + * @param xml_node The XML node to find. + * @return Returns xml_node if it is, otherwise returns null. + */ + private Node validateNode(Node root_node, Node xml_node) { + if (root_node == xml_node) { + return xml_node; + } else { + for (Node node = root_node.getFirstChild(); node != null; + node = node.getNextSibling()) { + if (root_node == xml_node || validateNode(node, xml_node) != null) { + return xml_node; + } + } + } + return null; + } + + /** + * Removes the <application> node from the hierarchy. + * Before doing that, we try to remember where it was so that we can put it back + * in the same place. + */ + private void removeApplicationNode() { + // Make sure the node actually exists... + Node xml_node = getUiElementNode().getXmlNode(); + if (xml_node == null) { + return; + } + + // Save its parent, next sibling and next element + mUndoXmlDocument = xml_node.getOwnerDocument(); + mUndoXmlParent = xml_node.getParentNode(); + mUndoXmlNextNode = xml_node.getNextSibling(); + mUndoXmlNextElement = mUndoXmlNextNode; + while (mUndoXmlNextElement != null && + mUndoXmlNextElement.getNodeType() != Node.ELEMENT_NODE) { + mUndoXmlNextElement = mUndoXmlNextElement.getNextSibling(); + } + + // Actually remove the node from the hierarchy and keep it here. + // The returned node looses its parents/siblings pointers. + mUndoXmlNode = getUiElementNode().deleteXmlNode(); + } + } + + /** + * This listener synchronizes the UI (i.e. the checkbox) with the + * actual presence of the application XML node. + */ + private class AppNodeUpdateListener implements IUiUpdateListener { + @Override + public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) { + // The UiElementNode for the application XML node always exists, even + // if there is no corresponding XML node in the XML file. + // + // To update the checkbox to reflect the actual state, we just need + // to check if the XML node is null. + try { + mInternalModification = true; + boolean exists = ui_node.getXmlNode() != null; + if (mCheckbox.getSelection() != exists) { + mCheckbox.setSelection(exists); + } + } finally { + mInternalModification = false; + } + + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/InstrumentationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/InstrumentationPage.java new file mode 100644 index 000000000..a8bb34691 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/InstrumentationPage.java @@ -0,0 +1,102 @@ +/* + * 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.pages; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.adt.internal.editors.ui.tree.UiTreeBlock; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.editor.FormPage; +import org.eclipse.ui.forms.widgets.ScrolledForm; + +/** + * Page for instrumentation settings, part of the AndroidManifest form editor. + */ +public final class InstrumentationPage extends FormPage implements IPageImageProvider { + /** Page id used for switching tabs programmatically */ + public final static String PAGE_ID = "instrumentation_page"; //$NON-NLS-1$ + + /** Container editor */ + ManifestEditor mEditor; + + private UiTreeBlock mTreeBlock; + + public InstrumentationPage(ManifestEditor editor) { + super(editor, PAGE_ID, "Instrumentation"); // tab's label, keep it short + mEditor = editor; + } + + @Override + public Image getPageImage() { + return IconFactory.getInstance().getIcon(getTitle(), + IconFactory.COLOR_GREEN, + IconFactory.SHAPE_RECT); + } + + /** + * Creates the content in the form hosted in this page. + * + * @param managedForm the form hosted in this page. + */ + @Override + protected void createFormContent(IManagedForm managedForm) { + super.createFormContent(managedForm); + ScrolledForm form = managedForm.getForm(); + form.setText("Android Manifest Instrumentation"); + form.setImage(AdtPlugin.getAndroidLogo()); + + UiElementNode manifest = mEditor.getUiRootNode(); + AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors(); + + ElementDescriptor[] descriptorFilters = null; + if (manifestDescriptor != null) { + descriptorFilters = new ElementDescriptor[] { + manifestDescriptor.getInstrumentationElement(), + }; + } + + mTreeBlock = new UiTreeBlock(mEditor, manifest, + true /* autoCreateRoot */, + descriptorFilters, + "Instrumentation", + "List of instrumentations defined in the manifest"); + mTreeBlock.createContent(managedForm); + } + + /** + * Changes and refreshes the Application UI node handled by the sub parts. + */ + public void refreshUiNode() { + if (mTreeBlock != null) { + UiElementNode manifest = mEditor.getUiRootNode(); + AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors(); + + mTreeBlock.changeRootAndDescriptors(manifest, + new ElementDescriptor[] { + manifestDescriptor.getInstrumentationElement() + }, + true /* refresh */); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewExportPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewExportPart.java new file mode 100644 index 000000000..b0eb75a2d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewExportPart.java @@ -0,0 +1,123 @@ +/* + * 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.pages; + +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart; +import com.android.ide.eclipse.adt.internal.project.ExportHelper; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.wizard.WizardDialog; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.PlatformUI; +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.Section; +import org.eclipse.ui.part.FileEditorInput; + +/** + * Export section part for overview page. + */ +final class OverviewExportPart extends ManifestSectionPart { + + private final OverviewPage mOverviewPage; + + public OverviewExportPart(OverviewPage overviewPage, final Composite body, FormToolkit toolkit, + ManifestEditor editor) { + super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */); + mOverviewPage = overviewPage; + Section section = getSection(); + section.setText("Exporting"); + + final IProject project = getProject(); + boolean isLibrary = false; + if (project != null) { + ProjectState state = Sdk.getProjectState(project); + if (state != null) { + isLibrary = state.isLibrary(); + } + } + + if (isLibrary) { + section.setDescription("Library project cannot be exported."); + Composite table = createTableLayout(toolkit, 2 /* numColumns */); + createFormText(table, toolkit, true, "<form></form>", false /* setupLayoutData */); + } else { + section.setDescription("To export the application for distribution, you have the following options:"); + + Composite table = createTableLayout(toolkit, 2 /* numColumns */); + + StringBuffer buf = new StringBuffer(); + buf.append("<form><li><a href=\"wizard\">"); //$NON-NLS-1$ + buf.append("Use the Export Wizard"); + buf.append("</a>"); //$NON-NLS-1$ + buf.append(" to export and sign an APK"); + buf.append("</li>"); //$NON-NLS-1$ + buf.append("<li><a href=\"manual\">"); //$NON-NLS-1$ + buf.append("Export an unsigned APK"); + buf.append("</a>"); //$NON-NLS-1$ + buf.append(" and sign it manually"); + buf.append("</li></form>"); //$NON-NLS-1$ + + FormText text = createFormText(table, toolkit, true, buf.toString(), + false /* setupLayoutData */); + text.addHyperlinkListener(new HyperlinkAdapter() { + @Override + public void linkActivated(HyperlinkEvent e) { + if (project != null) { + if ("manual".equals(e.data)) { //$NON-NLS-1$ + // now we can export an unsigned apk for the project. + ExportHelper.exportUnsignedReleaseApk(project); + } else { + // call the export wizard + StructuredSelection selection = new StructuredSelection(project); + + ExportWizard wizard = new ExportWizard(); + wizard.init(PlatformUI.getWorkbench(), selection); + WizardDialog dialog = new WizardDialog(body.getShell(), wizard); + dialog.open(); + } + } + } + }); + } + + layoutChanged(); + } + + /** + * Returns the project of the edited file. + */ + private IProject getProject() { + IEditorInput input = mOverviewPage.mEditor.getEditorInput(); + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput)input; + IFile file = fileInput.getFile(); + return file.getProject(); + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewInfoPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewInfoPart.java new file mode 100644 index 000000000..98f2f9cc2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewInfoPart.java @@ -0,0 +1,87 @@ +/* + * 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.pages; + +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.adt.internal.editors.ui.UiElementPart; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; + +/** + * Generic info section part for overview page: it displays all the attributes from + * the manifest element. + */ +final class OverviewInfoPart extends UiElementPart { + + private IManagedForm mManagedForm; + + public OverviewInfoPart(Composite body, FormToolkit toolkit, ManifestEditor editor) { + super(body, toolkit, editor, + getManifestUiNode(editor), // uiElementNode + "Manifest General Attributes", // section title + "Defines general information about the AndroidManifest.xml", // section description + Section.TWISTIE | Section.EXPANDED); + } + + /** + * Retrieves the UiElementNode that this part will edit. The node must exist + * and can't be null, by design, because it's a mandatory node. + */ + private static UiElementNode getManifestUiNode(ManifestEditor editor) { + AndroidManifestDescriptors manifestDescriptors = editor.getManifestDescriptors(); + if (manifestDescriptors != null) { + ElementDescriptor desc = manifestDescriptors.getManifestElement(); + if (editor.getUiRootNode().getDescriptor() == desc) { + return editor.getUiRootNode(); + } else { + return editor.getUiRootNode().findUiChildNode(desc.getXmlName()); + } + } + + // No manifest descriptor: we have a dummy UiRootNode, so we return that. + // The editor will be reloaded once we have the proper descriptors anyway. + return editor.getUiRootNode(); + } + + /** + * Overridden in order to capture the current managed form. + * + * {@inheritDoc} + */ + @Override + protected void createFormControls(final IManagedForm managedForm) { + mManagedForm = managedForm; + super.createFormControls(managedForm); + } + + /** + * Removes any existing Attribute UI widgets and recreate them if the SDK has changed. + * <p/> + * This is called by {@link OverviewPage#refreshUiApplicationNode()} when the + * SDK has changed. + */ + public void onSdkChanged() { + setUiElementNode(getManifestUiNode(getEditor())); + createUiAttributes(mManagedForm); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewLinksPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewLinksPart.java new file mode 100644 index 000000000..f8213753a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewLinksPart.java @@ -0,0 +1,124 @@ +/* + * 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.pages; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.forms.widgets.FormText; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; + +/** + * Links section part for overview page. + */ +final class OverviewLinksPart extends ManifestSectionPart { + + private final ManifestEditor mEditor; + private FormText mFormText; + + public OverviewLinksPart(Composite body, FormToolkit toolkit, ManifestEditor editor) { + super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */); + mEditor = editor; + Section section = getSection(); + section.setText("Links"); + section.setDescription("The content of the Android Manifest is made up of three sections. You can also edit the XML directly."); + + Composite table = createTableLayout(toolkit, 2 /* numColumns */); + + StringBuffer buf = new StringBuffer(); + buf.append(String.format("<form><li style=\"image\" value=\"app_img\"><a href=\"page:%1$s\">", //$NON-NLS-1$ + ApplicationPage.PAGE_ID)); + buf.append("Application"); + buf.append("</a>"); //$NON-NLS-1$ + buf.append(": Activities, intent filters, providers, services and receivers."); + buf.append("</li>"); //$NON-NLS-1$ + + buf.append(String.format("<li style=\"image\" value=\"perm_img\"><a href=\"page:%1$s\">", //$NON-NLS-1$ + PermissionPage.PAGE_ID)); + buf.append("Permission"); + buf.append("</a>"); //$NON-NLS-1$ + buf.append(": Permissions defined and permissions used."); + buf.append("</li>"); //$NON-NLS-1$ + + buf.append(String.format("<li style=\"image\" value=\"inst_img\"><a href=\"page:%1$s\">", //$NON-NLS-1$ + InstrumentationPage.PAGE_ID)); + buf.append("Instrumentation"); + buf.append("</a>"); //$NON-NLS-1$ + buf.append(": Instrumentation defined."); + buf.append("</li>"); //$NON-NLS-1$ + + buf.append(String.format("<li style=\"image\" value=\"srce_img\"><a href=\"page:%1$s\">", //$NON-NLS-1$ + ManifestEditor.TEXT_EDITOR_ID)); + buf.append("XML Source"); + buf.append("</a>"); //$NON-NLS-1$ + buf.append(": Directly edit the AndroidManifest.xml file."); + buf.append("</li>"); //$NON-NLS-1$ + + buf.append("<li style=\"image\" value=\"android_img\">"); //$NON-NLS-1$ + buf.append("<a href=\"http://code.google.com/android/devel/bblocks-manifest.html\">Documentation</a>: Documentation from the Android SDK for AndroidManifest.xml."); //$NON-NLS-1$ + buf.append("</li>"); //$NON-NLS-1$ + buf.append("</form>"); //$NON-NLS-1$ + + mFormText = createFormText(table, toolkit, true, buf.toString(), + false /* setupLayoutData */); + + AndroidManifestDescriptors manifestDescriptor = editor.getManifestDescriptors(); + + Image androidLogo = AdtPlugin.getAndroidLogo(); + mFormText.setImage("android_img", androidLogo); //$NON-NLS-1$ + mFormText.setImage("srce_img", IconFactory.getInstance().getIcon(AndroidXmlEditor.ICON_XML_PAGE)); + + if (manifestDescriptor != null) { + mFormText.setImage("app_img", getIcon(manifestDescriptor.getApplicationElement())); //$NON-NLS-1$ + mFormText.setImage("perm_img", getIcon(manifestDescriptor.getPermissionElement())); //$NON-NLS-1$ + mFormText.setImage("inst_img", getIcon(manifestDescriptor.getInstrumentationElement())); //$NON-NLS-1$ + } else { + mFormText.setImage("app_img", androidLogo); //$NON-NLS-1$ + mFormText.setImage("perm_img", androidLogo); //$NON-NLS-1$ + mFormText.setImage("inst_img", androidLogo); //$NON-NLS-1$ + } + mFormText.addHyperlinkListener(editor.createHyperlinkListener()); + } + + /** + * Update the UI with information from the new descriptors. + * <p/>At this point, this only refreshes the icons. + * <p/> + * This is called by {@link OverviewPage#refreshUiApplicationNode()} when the + * SDK has changed. + */ + public void onSdkChanged() { + AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors(); + if (manifestDescriptor != null) { + mFormText.setImage("app_img", getIcon(manifestDescriptor.getApplicationElement())); //$NON-NLS-1$ + mFormText.setImage("perm_img", getIcon(manifestDescriptor.getPermissionElement())); //$NON-NLS-1$ + mFormText.setImage("inst_img", getIcon(manifestDescriptor.getInstrumentationElement())); //$NON-NLS-1$ + } + } + + private Image getIcon(ElementDescriptor desc) { + return desc.getCustomizedIcon(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewPage.java new file mode 100644 index 000000000..7464f6a5f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/OverviewPage.java @@ -0,0 +1,165 @@ +/* + * 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.pages; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.adt.internal.editors.ui.tree.UiTreeBlock; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.editor.FormPage; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.ScrolledForm; + +import java.util.ArrayList; +import java.util.HashSet; + + +/** + * Page for overview settings, part of the AndroidManifest form editor. + * <p/> + * Useful reference: + * <a href="http://www.eclipse.org/articles/Article-Forms/article.html"> + * http://www.eclipse.org/articles/Article-Forms/article.html</a> + */ +public final class OverviewPage extends FormPage implements IPageImageProvider { + + /** Page id used for switching tabs programmatically */ + final static String PAGE_ID = "overview_page"; //$NON-NLS-1$ + + /** Container editor */ + ManifestEditor mEditor; + /** Overview part (attributes for manifest) */ + private OverviewInfoPart mOverviewPart; + /** Overview link part */ + private OverviewLinksPart mOverviewLinkPart; + + private UiTreeBlock mTreeBlock; + + public OverviewPage(ManifestEditor editor) { + super(editor, PAGE_ID, "Manifest"); // tab's label, user visible, keep it short + mEditor = editor; + } + + @Override + public Image getPageImage() { + return IconFactory.getInstance().getIcon("editor_page_design"); //$NON-NLS-1$ + } + + /** + * Creates the content in the form hosted in this page. + * + * @param managedForm the form hosted in this page. + */ + @Override + protected void createFormContent(IManagedForm managedForm) { + super.createFormContent(managedForm); + ScrolledForm form = managedForm.getForm(); + form.setText("Android Manifest"); + form.setImage(AdtPlugin.getAndroidLogo()); + + Composite body = form.getBody(); + FormToolkit toolkit = managedForm.getToolkit(); + + // Usually we would set a ColumnLayout on body here. However the presence of the + // UiTreeBlock forces a GridLayout with one column so we comply with it. + + mOverviewPart = new OverviewInfoPart(body, toolkit, mEditor); + mOverviewPart.getSection().setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + managedForm.addPart(mOverviewPart); + + newManifestExtrasPart(managedForm); + + OverviewExportPart exportPart = new OverviewExportPart(this, body, toolkit, mEditor); + exportPart.getSection().setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + managedForm.addPart(exportPart); + + mOverviewLinkPart = new OverviewLinksPart(body, toolkit, mEditor); + mOverviewLinkPart.getSection().setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + managedForm.addPart(mOverviewLinkPart); + } + + private void newManifestExtrasPart(IManagedForm managedForm) { + UiElementNode manifest = mEditor.getUiRootNode(); + mTreeBlock = new UiTreeBlock(mEditor, manifest, + true /* autoCreateRoot */, + computeManifestExtraFilters(), + "Manifest Extras", + "Extra manifest elements"); + mTreeBlock.createContent(managedForm); + } + + /** + * Changes and refreshes the Application UI node handled by the sub parts. + */ + public void refreshUiApplicationNode() { + if (mOverviewPart != null) { + mOverviewPart.onSdkChanged(); + } + + if (mOverviewLinkPart != null) { + mOverviewLinkPart.onSdkChanged(); + } + + if (mTreeBlock != null) { + UiElementNode manifest = mEditor.getUiRootNode(); + mTreeBlock.changeRootAndDescriptors(manifest, + computeManifestExtraFilters(), + true /* refresh */); + } + } + + private ElementDescriptor[] computeManifestExtraFilters() { + UiElementNode manifest = mEditor.getUiRootNode(); + AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors(); + + if (manifestDescriptor == null) { + return null; + } + + // get the elements we want to exclude + HashSet<ElementDescriptor> excludes = new HashSet<ElementDescriptor>(); + excludes.add(manifestDescriptor.getApplicationElement()); + excludes.add(manifestDescriptor.getInstrumentationElement()); + excludes.add(manifestDescriptor.getPermissionElement()); + excludes.add(manifestDescriptor.getPermissionGroupElement()); + excludes.add(manifestDescriptor.getPermissionTreeElement()); + excludes.add(manifestDescriptor.getUsesPermissionElement()); + + // walk through the known children of the manifest descriptor and keep what's not excluded + ArrayList<ElementDescriptor> descriptorFilters = new ArrayList<ElementDescriptor>(); + for (ElementDescriptor child : manifest.getDescriptor().getChildren()) { + if (!excludes.contains(child)) { + descriptorFilters.add(child); + } + } + + if (descriptorFilters.size() == 0) { + return null; + } + return descriptorFilters.toArray(new ElementDescriptor[descriptorFilters.size()]); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/PermissionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/PermissionPage.java new file mode 100644 index 000000000..2f655777a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/pages/PermissionPage.java @@ -0,0 +1,111 @@ +/* + * 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.pages; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.adt.internal.editors.ui.tree.UiTreeBlock; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.editor.FormPage; +import org.eclipse.ui.forms.widgets.ScrolledForm; + +/** + * Page for permissions settings, part of the AndroidManifest form editor. + * <p/> + * Useful reference: + * <a href="http://www.eclipse.org/articles/Article-Forms/article.html"> + * http://www.eclipse.org/articles/Article-Forms/article.html</a> + */ +public final class PermissionPage extends FormPage implements IPageImageProvider { + /** Page id used for switching tabs programmatically */ + public final static String PAGE_ID = "permission_page"; //$NON-NLS-1$ + + /** Container editor */ + ManifestEditor mEditor; + + private UiTreeBlock mTreeBlock; + + public PermissionPage(ManifestEditor editor) { + super(editor, PAGE_ID, "Permissions"); // tab label, keep it short + mEditor = editor; + } + + @Override + public Image getPageImage() { + return IconFactory.getInstance().getIcon(getTitle(), + IconFactory.COLOR_RED, + IconFactory.SHAPE_RECT); + } + + /** + * Creates the content in the form hosted in this page. + * + * @param managedForm the form hosted in this page. + */ + @Override + protected void createFormContent(IManagedForm managedForm) { + super.createFormContent(managedForm); + ScrolledForm form = managedForm.getForm(); + form.setText("Android Manifest Permissions"); + form.setImage(AdtPlugin.getAndroidLogo()); + + UiElementNode manifest = mEditor.getUiRootNode(); + AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors(); + + ElementDescriptor[] descriptorFilters = null; + if (manifestDescriptor != null) { + descriptorFilters = new ElementDescriptor[] { + manifestDescriptor.getPermissionElement(), + manifestDescriptor.getUsesPermissionElement(), + manifestDescriptor.getPermissionGroupElement(), + manifestDescriptor.getPermissionTreeElement() + }; + } + mTreeBlock = new UiTreeBlock(mEditor, manifest, + true /* autoCreateRoot */, + descriptorFilters, + "Permissions", + "List of permissions defined and used by the manifest"); + mTreeBlock.createContent(managedForm); + } + + /** + * Changes and refreshes the Application UI node handled by the sub parts. + */ + public void refreshUiNode() { + if (mTreeBlock != null) { + UiElementNode manifest = mEditor.getUiRootNode(); + AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors(); + + mTreeBlock.changeRootAndDescriptors(manifest, + new ElementDescriptor[] { + manifestDescriptor.getPermissionElement(), + manifestDescriptor.getUsesPermissionElement(), + manifestDescriptor.getPermissionGroupElement(), + manifestDescriptor.getPermissionTreeElement() + }, + true /* refresh */); + } + } +} |