diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre')
9 files changed, 4243 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java new file mode 100644 index 000000000..388907a46 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java @@ -0,0 +1,762 @@ +/* + * 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.layout.gre; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_ID; +import static com.android.SdkConstants.AUTO_URI; +import static com.android.SdkConstants.CLASS_FRAGMENT; +import static com.android.SdkConstants.CLASS_V4_FRAGMENT; +import static com.android.SdkConstants.CLASS_VIEW; +import static com.android.SdkConstants.NEW_ID_PREFIX; +import static com.android.SdkConstants.URI_PREFIX; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.api.IClientRulesEngine; +import com.android.ide.common.api.INode; +import com.android.ide.common.api.IValidator; +import com.android.ide.common.api.IViewMetadata; +import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.Margins; +import com.android.ide.common.api.Rect; +import com.android.ide.common.layout.BaseViewRule; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult; +import com.android.ide.eclipse.adt.internal.resources.CyclicDependencyValidator; +import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +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.ui.MarginChooser; +import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog; +import com.android.ide.eclipse.adt.internal.ui.ResourceChooser; +import com.android.ide.eclipse.adt.internal.ui.ResourcePreviewHelper; +import com.android.resources.ResourceType; +import com.android.sdklib.IAndroidTarget; + +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.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.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.TypeSelectionExtension; +import org.eclipse.jdt.ui.wizards.NewClassWizardPage; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.IInputValidator; +import org.eclipse.jface.dialogs.InputDialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +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.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.dialogs.SelectionDialog; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Implementation of {@link IClientRulesEngine}. This provides {@link IViewRule} clients + * with a few methods they can use to access functionality from this {@link RulesEngine}. + */ +class ClientRulesEngine implements IClientRulesEngine { + /** The return code from the dialog for the user choosing "Clear" */ + public static final int CLEAR_RETURN_CODE = -5; + /** The dialog button ID for the user choosing "Clear" */ + private static final int CLEAR_BUTTON_ID = CLEAR_RETURN_CODE; + + private final RulesEngine mRulesEngine; + private final String mFqcn; + + public ClientRulesEngine(RulesEngine rulesEngine, String fqcn) { + mRulesEngine = rulesEngine; + mFqcn = fqcn; + } + + @Override + public @NonNull String getFqcn() { + return mFqcn; + } + + @Override + public void debugPrintf(@NonNull String msg, Object... params) { + AdtPlugin.printToConsole( + mFqcn == null ? "<unknown>" : mFqcn, + String.format(msg, params) + ); + } + + @Override + public IViewRule loadRule(@NonNull String fqcn) { + return mRulesEngine.loadRule(fqcn, fqcn); + } + + @Override + public void displayAlert(@NonNull String message) { + MessageDialog.openInformation( + AdtPlugin.getShell(), + mFqcn, // title + message); + } + + @Override + public boolean rename(INode node) { + GraphicalEditorPart editor = mRulesEngine.getEditor(); + SelectionManager manager = editor.getCanvasControl().getSelectionManager(); + RenameResult result = manager.performRename(node, null); + + return !result.isCanceled() && !result.isUnavailable(); + } + + @Override + public String displayInput(@NonNull String message, @Nullable String value, + final @Nullable IValidator filter) { + IInputValidator validator = null; + if (filter != null) { + validator = new IInputValidator() { + @Override + public String isValid(String newText) { + // IValidator has the same interface as SWT's IInputValidator + try { + return filter.validate(newText); + } catch (Exception e) { + AdtPlugin.log(e, "Custom validator failed: %s", e.toString()); + return ""; //$NON-NLS-1$ + } + } + }; + } + + InputDialog d = new InputDialog( + AdtPlugin.getShell(), + mFqcn, // title + message, + value == null ? "" : value, //$NON-NLS-1$ + validator) { + @Override + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, CLEAR_BUTTON_ID, "Clear", false /*defaultButton*/); + super.createButtonsForButtonBar(parent); + } + + @Override + protected void buttonPressed(int buttonId) { + super.buttonPressed(buttonId); + + if (buttonId == CLEAR_BUTTON_ID) { + assert CLEAR_RETURN_CODE != Window.OK && CLEAR_RETURN_CODE != Window.CANCEL; + setReturnCode(CLEAR_RETURN_CODE); + close(); + } + } + }; + int result = d.open(); + if (result == ResourceChooser.CLEAR_RETURN_CODE) { + return ""; + } else if (result == Window.OK) { + return d.getValue(); + } + return null; + } + + @Override + @Nullable + public Object getViewObject(@NonNull INode node) { + ViewHierarchy views = mRulesEngine.getEditor().getCanvasControl().getViewHierarchy(); + CanvasViewInfo vi = views.findViewInfoFor(node); + if (vi != null) { + return vi.getViewObject(); + } + + return null; + } + + @Override + public @NonNull IViewMetadata getMetadata(final @NonNull String fqcn) { + return new IViewMetadata() { + @Override + public @NonNull String getDisplayName() { + // This also works when there is no "." + return fqcn.substring(fqcn.lastIndexOf('.') + 1); + } + + @Override + public @NonNull FillPreference getFillPreference() { + return ViewMetadataRepository.get().getFillPreference(fqcn); + } + + @Override + public @NonNull Margins getInsets() { + return mRulesEngine.getEditor().getCanvasControl().getInsets(fqcn); + } + + @Override + public @NonNull List<String> getTopAttributes() { + return ViewMetadataRepository.get().getTopAttributes(fqcn); + } + }; + } + + @Override + public int getMinApiLevel() { + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + IAndroidTarget target = currentSdk.getTarget(mRulesEngine.getEditor().getProject()); + if (target != null) { + return target.getVersion().getApiLevel(); + } + } + + return -1; + } + + @Override + public IValidator getResourceValidator( + @NonNull final String resourceTypeName, final boolean uniqueInProject, + final boolean uniqueInLayout, final boolean exists, final String... allowed) { + return new IValidator() { + private ResourceNameValidator mValidator; + + @Override + public String validate(@NonNull String text) { + if (mValidator == null) { + ResourceType type = ResourceType.getEnum(resourceTypeName); + if (uniqueInLayout) { + assert !uniqueInProject; + assert !exists; + Set<String> existing = new HashSet<String>(); + Document doc = mRulesEngine.getEditor().getModel().getXmlDocument(); + if (doc != null) { + addIds(doc, existing); + } + for (String s : allowed) { + existing.remove(s); + } + mValidator = ResourceNameValidator.create(false, existing, type); + } else { + assert allowed.length == 0; + IProject project = mRulesEngine.getEditor().getProject(); + mValidator = ResourceNameValidator.create(false, project, type); + if (uniqueInProject) { + mValidator.unique(); + } + } + if (exists) { + mValidator.exist(); + } + } + + return mValidator.isValid(text); + } + }; + } + + /** Find declared ids under the given DOM node */ + private static void addIds(Node node, Set<String> ids) { + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) node; + String id = element.getAttributeNS(ANDROID_URI, ATTR_ID); + if (id != null && id.startsWith(NEW_ID_PREFIX)) { + ids.add(BaseViewRule.stripIdPrefix(id)); + } + } + + NodeList children = node.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + addIds(child, ids); + } + } + + @Override + public String displayReferenceInput(@Nullable String currentValue) { + GraphicalEditorPart graphicalEditor = mRulesEngine.getEditor(); + LayoutEditorDelegate delegate = graphicalEditor.getEditorDelegate(); + IProject project = delegate.getEditor().getProject(); + if (project != null) { + // get the resource repository for this project and the system resources. + ResourceRepository projectRepository = + ResourceManager.getInstance().getProjectResources(project); + Shell shell = AdtPlugin.getShell(); + if (shell == null) { + return null; + } + ReferenceChooserDialog dlg = new ReferenceChooserDialog( + project, + projectRepository, + shell); + dlg.setPreviewHelper(new ResourcePreviewHelper(dlg, graphicalEditor)); + + dlg.setCurrentResource(currentValue); + + if (dlg.open() == Window.OK) { + return dlg.getCurrentResource(); + } + } + + return null; + } + + @Override + public String displayResourceInput(@NonNull String resourceTypeName, + @Nullable String currentValue) { + return displayResourceInput(resourceTypeName, currentValue, null); + } + + private String displayResourceInput(String resourceTypeName, String currentValue, + IInputValidator validator) { + ResourceType type = ResourceType.getEnum(resourceTypeName); + GraphicalEditorPart graphicalEditor = mRulesEngine.getEditor(); + return ResourceChooser.chooseResource(graphicalEditor, type, currentValue, validator); + } + + @Override + public String[] displayMarginInput(@Nullable String all, @Nullable String left, + @Nullable String right, @Nullable String top, @Nullable String bottom) { + GraphicalEditorPart editor = mRulesEngine.getEditor(); + IProject project = editor.getProject(); + if (project != null) { + Shell shell = AdtPlugin.getShell(); + if (shell == null) { + return null; + } + AndroidTargetData data = editor.getEditorDelegate().getEditor().getTargetData(); + MarginChooser dialog = new MarginChooser(shell, editor, data, all, left, right, + top, bottom); + if (dialog.open() == Window.OK) { + return dialog.getMargins(); + } + } + + return null; + } + + @Override + public String displayIncludeSourceInput() { + AndroidXmlEditor editor = mRulesEngine.getEditor().getEditorDelegate().getEditor(); + IInputValidator validator = CyclicDependencyValidator.create(editor.getInputFile()); + return displayResourceInput(ResourceType.LAYOUT.getName(), null, validator); + } + + @Override + public void select(final @NonNull Collection<INode> nodes) { + LayoutCanvas layoutCanvas = mRulesEngine.getEditor().getCanvasControl(); + final SelectionManager selectionManager = layoutCanvas.getSelectionManager(); + selectionManager.select(nodes); + // ALSO run an async select since immediately after nodes are created they + // may not be selectable. We can't ONLY run an async exec since + // code may depend on operating on the selection. + layoutCanvas.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + selectionManager.select(nodes); + } + }); + } + + @Override + public String displayFragmentSourceInput() { + try { + // Compute a search scope: We need to merge all the subclasses + // android.app.Fragment and android.support.v4.app.Fragment + IJavaSearchScope scope = SearchEngine.createWorkspaceScope(); + IProject project = mRulesEngine.getProject(); + final IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + if (javaProject != null) { + IType oldFragmentType = javaProject.findType(CLASS_V4_FRAGMENT); + + // First check to make sure fragments are available, and if not, + // warn the user. + IAndroidTarget target = Sdk.getCurrent().getTarget(project); + // No, this should be using the min SDK instead! + if (target.getVersion().getApiLevel() < 11 && oldFragmentType == null) { + // Compatibility library must be present + MessageDialog dialog = + new MessageDialog( + Display.getCurrent().getActiveShell(), + "Fragment Warning", + null, + "Fragments require API level 11 or higher, or a compatibility " + + "library for older versions.\n\n" + + " Do you want to install the compatibility library?", + MessageDialog.QUESTION, + new String[] { "Install", "Cancel" }, + 1 /* default button: Cancel */); + int answer = dialog.open(); + if (answer == 0) { + if (!AddSupportJarAction.install(project)) { + return null; + } + } else { + return null; + } + } + + // Look up sub-types of each (new fragment class and compatibility fragment + // class, if any) and merge the two arrays - then create a scope from these + // elements. + IType[] fragmentTypes = new IType[0]; + IType[] oldFragmentTypes = new IType[0]; + if (oldFragmentType != null) { + ITypeHierarchy hierarchy = + oldFragmentType.newTypeHierarchy(new NullProgressMonitor()); + oldFragmentTypes = hierarchy.getAllSubtypes(oldFragmentType); + } + IType fragmentType = javaProject.findType(CLASS_FRAGMENT); + if (fragmentType != null) { + ITypeHierarchy hierarchy = + fragmentType.newTypeHierarchy(new NullProgressMonitor()); + fragmentTypes = hierarchy.getAllSubtypes(fragmentType); + } + IType[] subTypes = new IType[fragmentTypes.length + oldFragmentTypes.length]; + System.arraycopy(fragmentTypes, 0, subTypes, 0, fragmentTypes.length); + System.arraycopy(oldFragmentTypes, 0, subTypes, fragmentTypes.length, + oldFragmentTypes.length); + scope = SearchEngine.createJavaSearchScope(subTypes, IJavaSearchScope.SOURCES); + } + + Shell parent = AdtPlugin.getShell(); + final AtomicReference<String> returnValue = + new AtomicReference<String>(); + final AtomicReference<SelectionDialog> dialogHolder = + new AtomicReference<SelectionDialog>(); + final SelectionDialog dialog = JavaUI.createTypeDialog( + parent, + new ProgressMonitorDialog(parent), + scope, + IJavaElementSearchConstants.CONSIDER_CLASSES, false, + // Use ? as a default filter to fill dialog with matches + "?", //$NON-NLS-1$ + new TypeSelectionExtension() { + @Override + public Control createContentArea(Composite parentComposite) { + Composite composite = new Composite(parentComposite, SWT.NONE); + composite.setLayout(new GridLayout(1, false)); + Button button = new Button(composite, SWT.PUSH); + button.setText("Create New..."); + button.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + String fqcn = createNewFragmentClass(javaProject); + if (fqcn != null) { + returnValue.set(fqcn); + dialogHolder.get().close(); + } + } + }); + return composite; + } + + @Override + public ITypeInfoFilterExtension getFilterExtension() { + return new ITypeInfoFilterExtension() { + @Override + public boolean select(ITypeInfoRequestor typeInfoRequestor) { + int modifiers = typeInfoRequestor.getModifiers(); + if (!Flags.isPublic(modifiers) + || Flags.isInterface(modifiers) + || Flags.isEnum(modifiers) + || Flags.isAbstract(modifiers)) { + return false; + } + return true; + } + }; + } + }); + dialogHolder.set(dialog); + + dialog.setTitle("Choose Fragment Class"); + dialog.setMessage("Select a Fragment class (? = any character, * = any string):"); + if (dialog.open() == IDialogConstants.CANCEL_ID) { + return null; + } + if (returnValue.get() != null) { + return returnValue.get(); + } + + Object[] types = dialog.getResult(); + if (types != null && types.length > 0) { + return ((IType) types[0]).getFullyQualifiedName(); + } + } catch (JavaModelException e) { + AdtPlugin.log(e, null); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + return null; + } + + @Override + public String displayCustomViewClassInput() { + try { + IJavaSearchScope scope = SearchEngine.createWorkspaceScope(); + IProject project = mRulesEngine.getProject(); + final IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + if (javaProject != null) { + // Look up sub-types of each (new fragment class and compatibility fragment + // class, if any) and merge the two arrays - then create a scope from these + // elements. + IType[] viewTypes = new IType[0]; + IType fragmentType = javaProject.findType(CLASS_VIEW); + if (fragmentType != null) { + ITypeHierarchy hierarchy = + fragmentType.newTypeHierarchy(new NullProgressMonitor()); + viewTypes = hierarchy.getAllSubtypes(fragmentType); + } + scope = SearchEngine.createJavaSearchScope(viewTypes, IJavaSearchScope.SOURCES); + } + + Shell parent = AdtPlugin.getShell(); + final AtomicReference<String> returnValue = + new AtomicReference<String>(); + final AtomicReference<SelectionDialog> dialogHolder = + new AtomicReference<SelectionDialog>(); + final SelectionDialog dialog = JavaUI.createTypeDialog( + parent, + new ProgressMonitorDialog(parent), + scope, + IJavaElementSearchConstants.CONSIDER_CLASSES, false, + // Use ? as a default filter to fill dialog with matches + "?", //$NON-NLS-1$ + new TypeSelectionExtension() { + @Override + public Control createContentArea(Composite parentComposite) { + Composite composite = new Composite(parentComposite, SWT.NONE); + composite.setLayout(new GridLayout(1, false)); + Button button = new Button(composite, SWT.PUSH); + button.setText("Create New..."); + button.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + String fqcn = createNewCustomViewClass(javaProject); + if (fqcn != null) { + returnValue.set(fqcn); + dialogHolder.get().close(); + } + } + }); + return composite; + } + + @Override + public ITypeInfoFilterExtension getFilterExtension() { + return new ITypeInfoFilterExtension() { + @Override + public boolean select(ITypeInfoRequestor typeInfoRequestor) { + int modifiers = typeInfoRequestor.getModifiers(); + if (!Flags.isPublic(modifiers) + || Flags.isInterface(modifiers) + || Flags.isEnum(modifiers) + || Flags.isAbstract(modifiers)) { + return false; + } + return true; + } + }; + } + }); + dialogHolder.set(dialog); + + dialog.setTitle("Choose Custom View Class"); + dialog.setMessage("Select a Custom View class (? = any character, * = any string):"); + if (dialog.open() == IDialogConstants.CANCEL_ID) { + return null; + } + if (returnValue.get() != null) { + return returnValue.get(); + } + + Object[] types = dialog.getResult(); + if (types != null && types.length > 0) { + return ((IType) types[0]).getFullyQualifiedName(); + } + } catch (JavaModelException e) { + AdtPlugin.log(e, null); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + return null; + } + + @Override + public void redraw() { + mRulesEngine.getEditor().getCanvasControl().redraw(); + } + + @Override + public void layout() { + mRulesEngine.getEditor().recomputeLayout(); + } + + @Override + public Map<INode, Rect> measureChildren(@NonNull INode parent, + @Nullable IClientRulesEngine.AttributeFilter filter) { + RenderService renderService = RenderService.create(mRulesEngine.getEditor()); + Map<INode, Rect> map = renderService.measureChildren(parent, filter); + if (map == null) { + map = Collections.emptyMap(); + } + return map; + } + + @Override + public int pxToDp(int px) { + ConfigurationChooser chooser = mRulesEngine.getEditor().getConfigurationChooser(); + float dpi = chooser.getConfiguration().getDensity().getDpiValue(); + return (int) (px * 160 / dpi); + } + + @Override + public int dpToPx(int dp) { + ConfigurationChooser chooser = mRulesEngine.getEditor().getConfigurationChooser(); + float dpi = chooser.getConfiguration().getDensity().getDpiValue(); + return (int) (dp * dpi / 160); + } + + @Override + public int screenToLayout(int pixels) { + return (int) (pixels / mRulesEngine.getEditor().getCanvasControl().getScale()); + } + + private String createNewFragmentClass(IJavaProject javaProject) { + NewClassWizardPage page = new NewClassWizardPage(); + + IProject project = mRulesEngine.getProject(); + Sdk sdk = Sdk.getCurrent(); + if (sdk == null) { + return null; + } + IAndroidTarget target = sdk.getTarget(project); + String superClass; + if (target == null || target.getVersion().getApiLevel() < 11) { + superClass = CLASS_V4_FRAGMENT; + } else { + superClass = CLASS_FRAGMENT; + } + page.setSuperClass(superClass, true /* canBeModified */); + IPackageFragmentRoot root = ManifestInfo.getSourcePackageRoot(javaProject); + if (root != null) { + page.setPackageFragmentRoot(root, true /* canBeModified */); + } + ManifestInfo manifestInfo = ManifestInfo.get(project); + IPackageFragment pkg = manifestInfo.getPackageFragment(); + if (pkg != null) { + page.setPackageFragment(pkg, true /* canBeModified */); + } + OpenNewClassWizardAction action = new OpenNewClassWizardAction(); + action.setConfiguredWizardPage(page); + action.run(); + IType createdType = page.getCreatedType(); + if (createdType != null) { + return createdType.getFullyQualifiedName(); + } else { + return null; + } + } + + private String createNewCustomViewClass(IJavaProject javaProject) { + NewClassWizardPage page = new NewClassWizardPage(); + + IProject project = mRulesEngine.getProject(); + String superClass = CLASS_VIEW; + page.setSuperClass(superClass, true /* canBeModified */); + IPackageFragmentRoot root = ManifestInfo.getSourcePackageRoot(javaProject); + if (root != null) { + page.setPackageFragmentRoot(root, true /* canBeModified */); + } + ManifestInfo manifestInfo = ManifestInfo.get(project); + IPackageFragment pkg = manifestInfo.getPackageFragment(); + if (pkg != null) { + page.setPackageFragment(pkg, true /* canBeModified */); + } + OpenNewClassWizardAction action = new OpenNewClassWizardAction(); + action.setConfiguredWizardPage(page); + action.run(); + IType createdType = page.getCreatedType(); + if (createdType != null) { + return createdType.getFullyQualifiedName(); + } else { + return null; + } + } + + @Override + public @NonNull String getUniqueId(@NonNull String fqcn) { + UiDocumentNode root = mRulesEngine.getEditor().getModel(); + String prefix = fqcn.substring(fqcn.lastIndexOf('.') + 1); + prefix = Character.toLowerCase(prefix.charAt(0)) + prefix.substring(1); + return DescriptorsUtils.getFreeWidgetId(root, prefix); + } + + @Override + public @NonNull String getAppNameSpace() { + IProject project = mRulesEngine.getEditor().getProject(); + + ProjectState projectState = Sdk.getProjectState(project); + if (projectState != null && projectState.isLibrary()) { + return AUTO_URI; + } + + ManifestInfo info = ManifestInfo.get(project); + return URI_PREFIX + info.getPackage(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java new file mode 100644 index 000000000..b0b9971ba --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2010 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.layout.gre; + +import com.android.ide.common.api.INode; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; + +import org.eclipse.swt.graphics.Rectangle; + +import java.util.Map; +import java.util.WeakHashMap; + +/** + * An object that can create {@link INode} proxies. + * This also keeps references to objects already created and tries to reuse them. + */ +public class NodeFactory { + + private final Map<UiViewElementNode, NodeProxy> mNodeMap = + new WeakHashMap<UiViewElementNode, NodeProxy>(); + private LayoutCanvas mCanvas; + + public NodeFactory(LayoutCanvas canvas) { + mCanvas = canvas; + } + + /** + * Returns an {@link INode} proxy based on the view key of the given + * {@link CanvasViewInfo}. The bounds of the node are set to the canvas view bounds. + */ + public NodeProxy create(CanvasViewInfo canvasViewInfo) { + return create(canvasViewInfo.getUiViewNode(), canvasViewInfo.getAbsRect()); + } + + /** + * Returns an {@link INode} proxy based on a given {@link UiViewElementNode} that + * is not yet part of the canvas, typically those created by layout rules + * when generating new XML. + */ + public NodeProxy create(UiViewElementNode uiNode) { + return create(uiNode, null /*bounds*/); + } + + public void clear() { + mNodeMap.clear(); + } + + public LayoutCanvas getCanvas() { + return mCanvas; + } + + //---- + + private NodeProxy create(UiViewElementNode uiNode, Rectangle bounds) { + NodeProxy proxy = mNodeMap.get(uiNode); + + if (proxy == null) { + // Create a new proxy if the key doesn't exist + proxy = new NodeProxy(uiNode, bounds, this); + mNodeMap.put(uiNode, proxy); + + } else if (bounds != null && !SwtUtils.equals(proxy.getBounds(), bounds)) { + // Update the bounds if necessary + proxy.setBounds(bounds); + } + + return proxy; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java new file mode 100644 index 000000000..19d5e16b0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java @@ -0,0 +1,517 @@ +/* + * 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.layout.gre; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.common.api.INode; +import com.android.ide.common.api.INodeHandler; +import com.android.ide.common.api.Margins; +import com.android.ide.common.api.Rect; +import com.android.ide.common.resources.platform.AttributeInfo; +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.DescriptorsUtils; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleAttribute; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.adt.internal.project.SupportLibraryHelper; + +import org.eclipse.core.resources.IProject; +import org.eclipse.swt.graphics.Rectangle; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * + */ +public class NodeProxy implements INode { + private static final Margins NO_MARGINS = new Margins(0, 0, 0, 0); + private final UiViewElementNode mNode; + private final Rect mBounds; + private final NodeFactory mFactory; + /** Map from URI to Map(key=>value) (where no namespace uses "" as a key) */ + private Map<String, Map<String, String>> mPendingAttributes; + + /** + * Creates a new {@link INode} that wraps an {@link UiViewElementNode} that is + * actually valid in the current UI/XML model. The view may not be part of the canvas + * yet (e.g. if it has just been dynamically added and the canvas hasn't reloaded yet.) + * <p/> + * This method is package protected. To create a node, please use {@link NodeFactory} instead. + * + * @param uiNode The node to wrap. + * @param bounds The bounds of a the view in the canvas. Must be either: <br/> + * - a valid rect for a view that is actually in the canvas <br/> + * - <b>*or*</b> null (or an invalid rect) for a view that has just been added dynamically + * to the model. We never store a null bounds rectangle in the node, a null rectangle + * will be converted to an invalid rectangle. + * @param factory A {@link NodeFactory} to create unique children nodes. + */ + /*package*/ NodeProxy(UiViewElementNode uiNode, Rectangle bounds, NodeFactory factory) { + mNode = uiNode; + mFactory = factory; + if (bounds == null) { + mBounds = new Rect(); + } else { + mBounds = SwtUtils.toRect(bounds); + } + } + + @Override + public @NonNull Rect getBounds() { + return mBounds; + } + + @Override + public @NonNull Margins getMargins() { + ViewHierarchy viewHierarchy = mFactory.getCanvas().getViewHierarchy(); + CanvasViewInfo view = viewHierarchy.findViewInfoFor(this); + if (view != null) { + Margins margins = view.getMargins(); + if (margins != null) { + return margins; + } + } + + return NO_MARGINS; + } + + + @Override + public int getBaseline() { + ViewHierarchy viewHierarchy = mFactory.getCanvas().getViewHierarchy(); + CanvasViewInfo view = viewHierarchy.findViewInfoFor(this); + if (view != null) { + return view.getBaseline(); + } + + return -1; + } + + /** + * Updates the bounds of this node proxy. Bounds cannot be null, but it can be invalid. + * This is a package-protected method, only the {@link NodeFactory} uses this method. + */ + /*package*/ void setBounds(Rectangle bounds) { + SwtUtils.set(mBounds, bounds); + } + + /** + * Returns the {@link UiViewElementNode} corresponding to this + * {@link NodeProxy}. + * + * @return The {@link UiViewElementNode} corresponding to this + * {@link NodeProxy} + */ + public UiViewElementNode getNode() { + return mNode; + } + + @Override + public @NonNull String getFqcn() { + if (mNode != null) { + ElementDescriptor desc = mNode.getDescriptor(); + if (desc instanceof ViewElementDescriptor) { + return ((ViewElementDescriptor) desc).getFullClassName(); + } + } + + return ""; + } + + + // ---- Hierarchy handling ---- + + + @Override + public INode getRoot() { + if (mNode != null) { + UiElementNode p = mNode.getUiRoot(); + // The node root should be a document. Instead what we really mean to + // return is the top level view element. + if (p instanceof UiDocumentNode) { + List<UiElementNode> children = p.getUiChildren(); + if (children.size() > 0) { + p = children.get(0); + } + } + + // Cope with a badly structured XML layout + while (p != null && !(p instanceof UiViewElementNode)) { + p = p.getUiNextSibling(); + } + + if (p == mNode) { + return this; + } + if (p instanceof UiViewElementNode) { + return mFactory.create((UiViewElementNode) p); + } + } + + return null; + } + + @Override + public INode getParent() { + if (mNode != null) { + UiElementNode p = mNode.getUiParent(); + if (p instanceof UiViewElementNode) { + return mFactory.create((UiViewElementNode) p); + } + } + + return null; + } + + @Override + public @NonNull INode[] getChildren() { + if (mNode != null) { + List<UiElementNode> uiChildren = mNode.getUiChildren(); + List<INode> nodes = new ArrayList<INode>(uiChildren.size()); + for (UiElementNode uiChild : uiChildren) { + if (uiChild instanceof UiViewElementNode) { + nodes.add(mFactory.create((UiViewElementNode) uiChild)); + } + } + + return nodes.toArray(new INode[nodes.size()]); + } + + return new INode[0]; + } + + + // ---- XML Editing --- + + @Override + public void editXml(@NonNull String undoName, final @NonNull INodeHandler c) { + final AndroidXmlEditor editor = mNode.getEditor(); + + if (editor != null) { + // Create an undo edit XML wrapper, which takes a runnable + editor.wrapUndoEditXmlModel( + undoName, + new Runnable() { + @Override + public void run() { + // Here editor.isEditXmlModelPending returns true and it + // is safe to edit the model using any method from INode. + + // Finally execute the closure that will act on the XML + c.handle(NodeProxy.this); + applyPendingChanges(); + } + }); + } + } + + private void checkEditOK() { + final AndroidXmlEditor editor = mNode.getEditor(); + if (!editor.isEditXmlModelPending()) { + throw new RuntimeException("Error: XML edit call without using INode.editXml!"); + } + } + + @Override + public @NonNull INode appendChild(@NonNull String viewFqcn) { + return insertOrAppend(viewFqcn, -1); + } + + @Override + public @NonNull INode insertChildAt(@NonNull String viewFqcn, int index) { + return insertOrAppend(viewFqcn, index); + } + + @Override + public void removeChild(@NonNull INode node) { + checkEditOK(); + + ((NodeProxy) node).mNode.deleteXmlNode(); + } + + private INode insertOrAppend(String viewFqcn, int index) { + checkEditOK(); + + AndroidXmlEditor editor = mNode.getEditor(); + if (editor != null) { + // Possibly replace the tag with a compatibility version if the + // minimum SDK requires it + IProject project = editor.getProject(); + if (project != null) { + viewFqcn = SupportLibraryHelper.getTagFor(project, viewFqcn); + } + } + + // Find the descriptor for this FQCN + ViewElementDescriptor vd = getFqcnViewDescriptor(viewFqcn); + if (vd == null) { + warnPrintf("Can't create a new %s element", viewFqcn); + return null; + } + + final UiElementNode uiNew; + if (index == -1) { + // Append at the end. + uiNew = mNode.appendNewUiChild(vd); + } else { + // Insert at the requested position or at the end. + int n = mNode.getUiChildren().size(); + if (index < 0 || index >= n) { + uiNew = mNode.appendNewUiChild(vd); + } else { + uiNew = mNode.insertNewUiChild(index, vd); + } + } + + // Set default attributes -- but only for new widgets (not when moving or copying) + RulesEngine engine = null; + LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(editor); + if (delegate != null) { + engine = delegate.getRulesEngine(); + } + if (engine == null || engine.getInsertType().isCreate()) { + // TODO: This should probably use IViewRule#getDefaultAttributes() at some point + DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/); + } + + Node xmlNode = uiNew.createXmlNode(); + + if (!(uiNew instanceof UiViewElementNode) || xmlNode == null) { + // Both things are not supposed to happen. When they do, we're in big trouble. + // We don't really know how to revert the state at this point and the UI model is + // now out of sync with the XML model. + // Panic ensues. + // The best bet is to abort now. The edit wrapper will release the edit and the + // XML/UI should get reloaded properly (with a likely invalid XML.) + warnPrintf("Failed to create a new %s element", viewFqcn); + throw new RuntimeException("XML node creation failed."); //$NON-NLS-1$ + } + + UiViewElementNode uiNewView = (UiViewElementNode) uiNew; + NodeProxy newNode = mFactory.create(uiNewView); + + if (engine != null) { + engine.callCreateHooks(editor, this, newNode, null); + } + + return newNode; + } + + @Override + public boolean setAttribute( + @Nullable String uri, + @NonNull String name, + @Nullable String value) { + checkEditOK(); + UiAttributeNode attr = mNode.setAttributeValue(name, uri, value, true /* override */); + + if (uri == null) { + uri = ""; //$NON-NLS-1$ + } + + Map<String, String> map = null; + if (mPendingAttributes == null) { + // Small initial size: we don't expect many different namespaces + mPendingAttributes = new HashMap<String, Map<String, String>>(3); + } else { + map = mPendingAttributes.get(uri); + } + if (map == null) { + map = new HashMap<String, String>(); + mPendingAttributes.put(uri, map); + } + map.put(name, value); + + return attr != null; + } + + @Override + public String getStringAttr(@Nullable String uri, @NonNull String attrName) { + UiElementNode uiNode = mNode; + + if (attrName == null) { + return null; + } + + if (mPendingAttributes != null) { + Map<String, String> map = mPendingAttributes.get(uri == null ? "" : uri); //$NON-NLS-1$ + if (map != null) { + String value = map.get(attrName); + if (value != null) { + return value; + } + } + } + + if (uiNode.getXmlNode() != null) { + Node xmlNode = uiNode.getXmlNode(); + if (xmlNode != null) { + NamedNodeMap nodeAttributes = xmlNode.getAttributes(); + if (nodeAttributes != null) { + Node attr = nodeAttributes.getNamedItemNS(uri, attrName); + if (attr != null) { + return attr.getNodeValue(); + } + } + } + } + return null; + } + + @Override + public IAttributeInfo getAttributeInfo(@Nullable String uri, @NonNull String attrName) { + UiElementNode uiNode = mNode; + + if (attrName == null) { + return null; + } + + for (AttributeDescriptor desc : uiNode.getAttributeDescriptors()) { + String dUri = desc.getNamespaceUri(); + String dName = desc.getXmlLocalName(); + if ((uri == null && dUri == null) || (uri != null && uri.equals(dUri))) { + if (attrName.equals(dName)) { + return desc.getAttributeInfo(); + } + } + } + + return null; + } + + @Override + public @NonNull IAttributeInfo[] getDeclaredAttributes() { + + AttributeDescriptor[] descs = mNode.getAttributeDescriptors(); + int n = descs.length; + IAttributeInfo[] infos = new AttributeInfo[n]; + + for (int i = 0; i < n; i++) { + infos[i] = descs[i].getAttributeInfo(); + } + + return infos; + } + + @Override + public @NonNull List<String> getAttributeSources() { + ElementDescriptor descriptor = mNode.getDescriptor(); + if (descriptor instanceof ViewElementDescriptor) { + return ((ViewElementDescriptor) descriptor).getAttributeSources(); + } else { + return Collections.emptyList(); + } + } + + @Override + public @NonNull IAttribute[] getLiveAttributes() { + UiElementNode uiNode = mNode; + + if (uiNode.getXmlNode() != null) { + Node xmlNode = uiNode.getXmlNode(); + if (xmlNode != null) { + NamedNodeMap nodeAttributes = xmlNode.getAttributes(); + if (nodeAttributes != null) { + + int n = nodeAttributes.getLength(); + IAttribute[] result = new IAttribute[n]; + for (int i = 0; i < n; i++) { + Node attr = nodeAttributes.item(i); + String uri = attr.getNamespaceURI(); + String name = attr.getLocalName(); + String value = attr.getNodeValue(); + + result[i] = new SimpleAttribute(uri, name, value); + } + return result; + } + } + } + + return new IAttribute[0]; + + } + + @Override + public String toString() { + return "NodeProxy [node=" + mNode + ", bounds=" + mBounds + "]"; + } + + // --- internal helpers --- + + /** + * Helper methods that returns a {@link ViewElementDescriptor} for the requested FQCN. + * Will return null if we can't find that FQCN or we lack the editor/data/descriptors info + * (which shouldn't really happen since at this point the SDK should be fully loaded and + * isn't reloading, or we wouldn't be here editing XML for a layout rule.) + */ + private ViewElementDescriptor getFqcnViewDescriptor(String fqcn) { + LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(mNode.getEditor()); + if (delegate != null) { + return delegate.getFqcnViewDescriptor(fqcn); + } + + return null; + } + + private void warnPrintf(String msg, Object...params) { + AdtPlugin.printToConsole( + mNode == null ? "" : mNode.getDescriptor().getXmlLocalName(), + String.format(msg, params) + ); + } + + /** + * If there are any pending changes in these nodes, apply them now + * + * @return true if any modifications were made + */ + public boolean applyPendingChanges() { + boolean modified = false; + + // Flush all pending attributes + if (mPendingAttributes != null) { + mNode.commitDirtyAttributesToXml(); + modified = true; + mPendingAttributes = null; + + } + for (INode child : getChildren()) { + modified |= ((NodeProxy) child).applyPendingChanges(); + } + + return modified; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/PaletteMetadataDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/PaletteMetadataDescriptor.java new file mode 100644 index 000000000..884cb077a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/PaletteMetadataDescriptor.java @@ -0,0 +1,120 @@ +/* + * 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.layout.gre; + +import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX; +import static com.android.SdkConstants.ANDROID_URI; + +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleAttribute; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleElement; + +import org.eclipse.swt.graphics.Image; +import org.w3c.dom.Element; + +/** + * Special version of {@link ViewElementDescriptor} which is initialized by the palette + * with specific metadata for how to instantiate particular variations of an existing + * {@link ViewElementDescriptor} with initial values. + */ +public class PaletteMetadataDescriptor extends ViewElementDescriptor { + private String mInitString; + private String mIconName; + + public PaletteMetadataDescriptor(ViewElementDescriptor descriptor, String displayName, + String initString, String iconName) { + super(descriptor.getXmlName(), + displayName, + descriptor.getFullClassName(), + descriptor.getTooltip(), + descriptor.getSdkUrl(), + descriptor.getAttributes(), + descriptor.getLayoutAttributes(), + descriptor.getChildren(), descriptor.getMandatory() == Mandatory.MANDATORY); + mInitString = initString; + mIconName = iconName; + setSuperClass(descriptor.getSuperClassDesc()); + } + + /** + * Returns a String which contains a comma separated list of name=value tokens, + * where the name can start with "android:" to indicate a property in the android namespace, + * or no prefix for plain attributes. + * + * @return the initialization string, which can be empty but never null + */ + public String getInitializedAttributes() { + return mInitString != null ? mInitString : ""; //$NON-NLS-1$ + } + + @Override + public Image getGenericIcon() { + if (mIconName != null) { + IconFactory factory = IconFactory.getInstance(); + Image icon = factory.getIcon(mIconName); + if (icon != null) { + return icon; + } + } + + return super.getGenericIcon(); + } + + /** + * Initializes a new {@link SimpleElement} with the palette initialization + * configuration + * + * @param element the new element to initialize + */ + public void initializeNew(SimpleElement element) { + initializeNew(element, null); + } + + /** + * Initializes a new {@link Element} with the palette initialization configuration + * + * @param element the new element to initialize + */ + public void initializeNew(Element element) { + initializeNew(null, element); + } + + private void initializeNew(SimpleElement simpleElement, Element domElement) { + String initializedAttributes = mInitString; + if (initializedAttributes != null && initializedAttributes.length() > 0) { + for (String s : initializedAttributes.split(",")) { //$NON-NLS-1$ + String[] nameValue = s.split("="); //$NON-NLS-1$ + String name = nameValue[0]; + String value = nameValue[1]; + String nameSpace = ""; //$NON-NLS-1$ + if (name.startsWith(ANDROID_NS_NAME_PREFIX)) { + name = name.substring(ANDROID_NS_NAME_PREFIX.length()); + nameSpace = ANDROID_URI; + } + + if (simpleElement != null) { + SimpleAttribute attr = new SimpleAttribute(nameSpace, name, value); + simpleElement.addAttribute(attr); + } + + if (domElement != null) { + domElement.setAttributeNS(nameSpace, name, value); + } + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RuleLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RuleLoader.java new file mode 100644 index 000000000..4f49a7545 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RuleLoader.java @@ -0,0 +1,192 @@ +/* + * 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.layout.gre; + +import com.android.ide.common.api.IViewRule; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.utils.Pair; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.QualifiedName; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; + +/** + * The {@link RuleLoader} is responsible for loading (and unloading) + * {@link IViewRule} classes. There is typically one {@link RuleLoader} + * per project. + */ +public class RuleLoader { + /** + * Qualified name for the per-project non-persistent property storing the + * {@link RuleLoader} for this project + */ + private final static QualifiedName RULE_LOADER = new QualifiedName(AdtPlugin.PLUGIN_ID, + "ruleloader"); //$NON-NLS-1$ + + private final IProject mProject; + private ClassLoader mUserClassLoader; + private List<Pair<File, Long>> mUserJarTimeStamps; + private long mLastCheckTimeStamp; + + /** + * Flag set when we've attempted to initialize the {@link #mUserClassLoader} + * already + */ + private boolean mUserClassLoaderInited; + + /** + * Returns the {@link RuleLoader} for the given project + * + * @param project the project the loader is associated with + * @return an {@RuleLoader} for the given project, + * never null + */ + public static RuleLoader get(IProject project) { + RuleLoader loader = null; + try { + loader = (RuleLoader) project.getSessionProperty(RULE_LOADER); + } catch (CoreException e) { + // Not a problem; we will just create a new one + } + if (loader == null) { + loader = new RuleLoader(project); + try { + project.setSessionProperty(RULE_LOADER, loader); + } catch (CoreException e) { + AdtPlugin.log(e, "Can't store RuleLoader"); + } + } + return loader; + } + + /** Do not call; use the {@link #get} factory method instead. */ + private RuleLoader(IProject project) { + mProject = project; + } + + /** + * Find out whether the given project has 3rd party ViewRules, and if so + * return a ClassLoader which can locate them. If not, return null. + * @param project The project to load user rules from + * @return A class loader which can user view rules, or otherwise null + */ + private ClassLoader computeUserClassLoader(IProject project) { + // Default place to locate layout rules. The user may also add to this + // path by defining a config property specifying + // additional .jar files to search via a the layoutrules.jars property. + ProjectState state = Sdk.getProjectState(project); + ProjectProperties projectProperties = state.getProperties(); + + // Ensure we have the latest & greatest version of the properties. + // This allows users to reopen editors in a running Eclipse instance + // to get updated view rule jars + projectProperties.reload(); + + String path = projectProperties.getProperty( + ProjectProperties.PROPERTY_RULES_PATH); + + if (path != null && path.length() > 0) { + + mUserJarTimeStamps = new ArrayList<Pair<File, Long>>(); + mLastCheckTimeStamp = System.currentTimeMillis(); + + List<URL> urls = new ArrayList<URL>(); + String[] pathElements = path.split(File.pathSeparator); + for (String pathElement : pathElements) { + pathElement = pathElement.trim(); // Avoid problems with trailing whitespace etc + File pathFile = new File(pathElement); + if (!pathFile.isAbsolute()) { + pathFile = new File(project.getLocation().toFile(), pathElement); + } + // Directories and jar files are okay. Do we need to + // validate the files here as .jar files? + if (pathFile.isFile() || pathFile.isDirectory()) { + URL url; + try { + url = pathFile.toURI().toURL(); + urls.add(url); + + mUserJarTimeStamps.add(Pair.of(pathFile, pathFile.lastModified())); + } catch (MalformedURLException e) { + AdtPlugin.log(IStatus.WARNING, + "Invalid URL: %1$s", //$NON-NLS-1$ + e.toString()); + } + } + } + + if (urls.size() > 0) { + return new URLClassLoader(urls.toArray(new URL[urls.size()]), + RulesEngine.class.getClassLoader()); + } + } + + return null; + } + + /** + * Return the class loader to use for custom views, or null if no custom + * view rules are registered for the project. Note that this class loader + * can change over time (if the jar files are updated), so callers should be + * prepared to unload previous instances. + * + * @return a class loader to use for custom view rules, or null + */ + public ClassLoader getClassLoader() { + if (mUserClassLoader == null) { + // Only attempt to load rule paths once. + // TODO: Check the timestamp on the project.properties file so we can dynamically + // pick up cases where the user edits the path + if (!mUserClassLoaderInited) { + mUserClassLoaderInited = true; + mUserClassLoader = computeUserClassLoader(mProject); + } + } else { + // Check the timestamp on the jar files in the custom view path to see if we + // need to reload the classes (but only do this at most every 3 seconds) + if (mUserJarTimeStamps != null) { + long time = System.currentTimeMillis(); + if (time - mLastCheckTimeStamp > 3000) { + mLastCheckTimeStamp = time; + for (Pair<File, Long> pair : mUserJarTimeStamps) { + File file = pair.getFirst(); + Long prevModified = pair.getSecond(); + long modified = file.lastModified(); + if (prevModified.longValue() != modified) { + mUserClassLoaderInited = true; + mUserJarTimeStamps = null; + mUserClassLoader = computeUserClassLoader(mProject); + } + } + } + } + } + + return mUserClassLoader; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java new file mode 100644 index 000000000..8f9923749 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java @@ -0,0 +1,876 @@ +/* + * 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.layout.gre; + +import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX; +import static com.android.SdkConstants.VIEW_MERGE; +import static com.android.SdkConstants.VIEW_TAG; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.api.DropFeedback; +import com.android.ide.common.api.IDragElement; +import com.android.ide.common.api.IGraphics; +import com.android.ide.common.api.INode; +import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.InsertType; +import com.android.ide.common.api.Point; +import com.android.ide.common.api.Rect; +import com.android.ide.common.api.RuleAction; +import com.android.ide.common.api.SegmentType; +import com.android.ide.common.layout.ViewRule; +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.layout.descriptors.ViewElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GCWrapper; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleElement; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.resources.IProject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +/** + * The rule engine manages the layout rules and interacts with them. + * There's one {@link RulesEngine} instance per layout editor. + * Each instance has 2 sets of rules: the static ADT rules (shared across all instances) + * and the project specific rules (local to the current instance / layout editor). + */ +public class RulesEngine { + private final IProject mProject; + private final Map<Object, IViewRule> mRulesCache = new HashMap<Object, IViewRule>(); + + /** + * The type of any upcoming node manipulations performed by the {@link IViewRule}s. + * When actions are performed in the tool (like a paste action, or a drag from palette, + * or a drag move within the canvas, etc), these are different types of inserts, + * and we don't want to have the rules track them closely (and pass them back to us + * in the {@link INode#insertChildAt} methods etc), so instead we track the state + * here on behalf of the currently executing rule. + */ + private InsertType mInsertType = InsertType.CREATE; + + /** + * Per-project loader for custom view rules + */ + private RuleLoader mRuleLoader; + private ClassLoader mUserClassLoader; + + /** + * The editor which owns this {@link RulesEngine} + */ + private final GraphicalEditorPart mEditor; + + /** + * Creates a new {@link RulesEngine} associated with the selected project. + * <p/> + * The rules engine will look in the project for a tools jar to load custom view rules. + * + * @param editor the editor which owns this {@link RulesEngine} + * @param project A non-null open project. + */ + public RulesEngine(GraphicalEditorPart editor, IProject project) { + mProject = project; + mEditor = editor; + + mRuleLoader = RuleLoader.get(project); + } + + /** + * Returns the {@link IProject} on which the {@link RulesEngine} was created. + */ + public IProject getProject() { + return mProject; + } + + /** + * Returns the {@link GraphicalEditorPart} for which the {@link RulesEngine} was + * created. + * + * @return the associated editor + */ + public GraphicalEditorPart getEditor() { + return mEditor; + } + + /** + * Called by the owner of the {@link RulesEngine} when it is going to be disposed. + * This frees some resources, such as the project's folder monitor. + */ + public void dispose() { + clearCache(); + } + + /** + * Invokes {@link IViewRule#getDisplayName()} on the rule matching the specified element. + * + * @param element The view element to target. Can be null. + * @return Null if the rule failed, there's no rule or the rule does not want to override + * the display name. Otherwise, a string as returned by the rule. + */ + public String callGetDisplayName(UiViewElementNode element) { + // try to find a rule for this element's FQCN + IViewRule rule = loadRule(element); + + if (rule != null) { + try { + return rule.getDisplayName(); + + } catch (Exception e) { + AdtPlugin.log(e, "%s.getDisplayName() failed: %s", + rule.getClass().getSimpleName(), + e.toString()); + } + } + + return null; + } + + /** + * Invokes {@link IViewRule#addContextMenuActions(List, INode)} on the rule matching the specified element. + * + * @param selectedNode The node selected. Never null. + * @return Null if the rule failed, there's no rule or the rule does not provide + * any custom menu actions. Otherwise, a list of {@link RuleAction}. + */ + @Nullable + public List<RuleAction> callGetContextMenu(NodeProxy selectedNode) { + // try to find a rule for this element's FQCN + IViewRule rule = loadRule(selectedNode.getNode()); + + if (rule != null) { + try { + mInsertType = InsertType.CREATE; + List<RuleAction> actions = new ArrayList<RuleAction>(); + rule.addContextMenuActions(actions, selectedNode); + Collections.sort(actions); + + return actions; + } catch (Exception e) { + AdtPlugin.log(e, "%s.getContextMenu() failed: %s", + rule.getClass().getSimpleName(), + e.toString()); + } + } + + return null; + } + + /** + * Calls the selected node to return its default action + * + * @param selectedNode the node to apply the action to + * @return the default action id + */ + public String callGetDefaultActionId(@NonNull NodeProxy selectedNode) { + // try to find a rule for this element's FQCN + IViewRule rule = loadRule(selectedNode.getNode()); + + if (rule != null) { + try { + mInsertType = InsertType.CREATE; + return rule.getDefaultActionId(selectedNode); + } catch (Exception e) { + AdtPlugin.log(e, "%s.getDefaultAction() failed: %s", + rule.getClass().getSimpleName(), + e.toString()); + } + } + + return null; + } + + /** + * Invokes {@link IViewRule#addLayoutActions(List, INode, List)} on the rule + * matching the specified element. + * + * @param actions The list of actions to add layout actions into + * @param parentNode The layout node + * @param children The selected children of the node, if any (used to + * initialize values of child layout controls, if applicable) + * @return Null if the rule failed, there's no rule or the rule does not + * provide any custom menu actions. Otherwise, a list of + * {@link RuleAction}. + */ + public List<RuleAction> callAddLayoutActions(List<RuleAction> actions, + NodeProxy parentNode, List<NodeProxy> children ) { + // try to find a rule for this element's FQCN + IViewRule rule = loadRule(parentNode.getNode()); + + if (rule != null) { + try { + mInsertType = InsertType.CREATE; + rule.addLayoutActions(actions, parentNode, children); + } catch (Exception e) { + AdtPlugin.log(e, "%s.getContextMenu() failed: %s", + rule.getClass().getSimpleName(), + e.toString()); + } + } + + return null; + } + + /** + * Invokes {@link IViewRule#getSelectionHint(INode, INode)} + * on the rule matching the specified element. + * + * @param parentNode The parent of the node selected. Never null. + * @param childNode The child node that was selected. Never null. + * @return a list of strings to be displayed, or null or empty to display nothing + */ + public List<String> callGetSelectionHint(NodeProxy parentNode, NodeProxy childNode) { + // try to find a rule for this element's FQCN + IViewRule rule = loadRule(parentNode.getNode()); + + if (rule != null) { + try { + return rule.getSelectionHint(parentNode, childNode); + + } catch (Exception e) { + AdtPlugin.log(e, "%s.getSelectionHint() failed: %s", + rule.getClass().getSimpleName(), + e.toString()); + } + } + + return null; + } + + public void callPaintSelectionFeedback(GCWrapper gcWrapper, NodeProxy parentNode, + List<? extends INode> childNodes, Object view) { + // try to find a rule for this element's FQCN + IViewRule rule = loadRule(parentNode.getNode()); + + if (rule != null) { + try { + rule.paintSelectionFeedback(gcWrapper, parentNode, childNodes, view); + + } catch (Exception e) { + AdtPlugin.log(e, "%s.callPaintSelectionFeedback() failed: %s", + rule.getClass().getSimpleName(), + e.toString()); + } + } + } + + /** + * Called when the d'n'd starts dragging over the target node. + * If interested, returns a DropFeedback passed to onDrop/Move/Leave/Paint. + * If not interested in drop, return false. + * Followed by a paint. + */ + public DropFeedback callOnDropEnter(NodeProxy targetNode, + Object targetView, IDragElement[] elements) { + // try to find a rule for this element's FQCN + IViewRule rule = loadRule(targetNode.getNode()); + + if (rule != null) { + try { + return rule.onDropEnter(targetNode, targetView, elements); + + } catch (Exception e) { + AdtPlugin.log(e, "%s.onDropEnter() failed: %s", + rule.getClass().getSimpleName(), + e.toString()); + } + } + + return null; + } + + /** + * Called after onDropEnter. + * Returns a DropFeedback passed to onDrop/Move/Leave/Paint (typically same + * as input one). + */ + public DropFeedback callOnDropMove(NodeProxy targetNode, + IDragElement[] elements, + DropFeedback feedback, + Point where) { + // try to find a rule for this element's FQCN + IViewRule rule = loadRule(targetNode.getNode()); + + if (rule != null) { + try { + return rule.onDropMove(targetNode, elements, feedback, where); + + } catch (Exception e) { + AdtPlugin.log(e, "%s.onDropMove() failed: %s", + rule.getClass().getSimpleName(), + e.toString()); + } + } + + return null; + } + + /** + * Called when drop leaves the target without actually dropping + */ + public void callOnDropLeave(NodeProxy targetNode, + IDragElement[] elements, + DropFeedback feedback) { + // try to find a rule for this element's FQCN + IViewRule rule = loadRule(targetNode.getNode()); + + if (rule != null) { + try { + rule.onDropLeave(targetNode, elements, feedback); + + } catch (Exception e) { + AdtPlugin.log(e, "%s.onDropLeave() failed: %s", + rule.getClass().getSimpleName(), + e.toString()); + } + } + } + + /** + * Called when drop is released over the target to perform the actual drop. + */ + public void callOnDropped(NodeProxy targetNode, + IDragElement[] elements, + DropFeedback feedback, + Point where, + InsertType insertType) { + // try to find a rule for this element's FQCN + IViewRule rule = loadRule(targetNode.getNode()); + + if (rule != null) { + try { + mInsertType = insertType; + rule.onDropped(targetNode, elements, feedback, where); + + } catch (Exception e) { + AdtPlugin.log(e, "%s.onDropped() failed: %s", + rule.getClass().getSimpleName(), + e.toString()); + } + } + } + + /** + * Called when a paint has been requested via DropFeedback. + */ + public void callDropFeedbackPaint(IGraphics gc, + NodeProxy targetNode, + DropFeedback feedback) { + if (gc != null && feedback != null && feedback.painter != null) { + try { + feedback.painter.paint(gc, targetNode, feedback); + } catch (Exception e) { + AdtPlugin.log(e, "DropFeedback.painter failed: %s", + e.toString()); + } + } + } + + /** + * Called when pasting elements in an existing document on the selected target. + * + * @param targetNode The first node selected. + * @param targetView The view object for the target node, or null if not known + * @param pastedElements The elements being pasted. + * @return the parent node the paste was applied into + */ + public NodeProxy callOnPaste(NodeProxy targetNode, Object targetView, + SimpleElement[] pastedElements) { + + // Find a target which accepts children. If you for example select a button + // and attempt to paste, this will reselect the parent of the button as the paste + // target. (This is a loop rather than just checking the direct parent since + // we will soon ask each child whether they are *willing* to accept the new child. + // A ScrollView for example, which only accepts one child, might also say no + // and delegate to its parent in turn. + INode parent = targetNode; + while (parent instanceof NodeProxy) { + NodeProxy np = (NodeProxy) parent; + if (np.getNode() != null && np.getNode().getDescriptor() != null) { + ElementDescriptor descriptor = np.getNode().getDescriptor(); + if (descriptor.hasChildren()) { + targetNode = np; + break; + } + } + parent = parent.getParent(); + } + + // try to find a rule for this element's FQCN + IViewRule rule = loadRule(targetNode.getNode()); + + if (rule != null) { + try { + mInsertType = InsertType.PASTE; + rule.onPaste(targetNode, targetView, pastedElements); + + } catch (Exception e) { + AdtPlugin.log(e, "%s.onPaste() failed: %s", + rule.getClass().getSimpleName(), + e.toString()); + } + } + + return targetNode; + } + + // ---- Resize operations ---- + + public DropFeedback callOnResizeBegin(NodeProxy child, NodeProxy parent, Rect newBounds, + SegmentType horizontalEdge, SegmentType verticalEdge, Object childView, + Object parentView) { + IViewRule rule = loadRule(parent.getNode()); + + if (rule != null) { + try { + return rule.onResizeBegin(child, parent, horizontalEdge, verticalEdge, + childView, parentView); + } catch (Exception e) { + AdtPlugin.log(e, "%s.onResizeBegin() failed: %s", rule.getClass().getSimpleName(), + e.toString()); + } + } + + return null; + } + + public void callOnResizeUpdate(DropFeedback feedback, NodeProxy child, NodeProxy parent, + Rect newBounds, int modifierMask) { + IViewRule rule = loadRule(parent.getNode()); + + if (rule != null) { + try { + rule.onResizeUpdate(feedback, child, parent, newBounds, modifierMask); + } catch (Exception e) { + AdtPlugin.log(e, "%s.onResizeUpdate() failed: %s", rule.getClass().getSimpleName(), + e.toString()); + } + } + } + + public void callOnResizeEnd(DropFeedback feedback, NodeProxy child, NodeProxy parent, + Rect newBounds) { + IViewRule rule = loadRule(parent.getNode()); + + if (rule != null) { + try { + rule.onResizeEnd(feedback, child, parent, newBounds); + } catch (Exception e) { + AdtPlugin.log(e, "%s.onResizeEnd() failed: %s", rule.getClass().getSimpleName(), + e.toString()); + } + } + } + + // ---- Creation customizations ---- + + /** + * Invokes the create hooks ({@link IViewRule#onCreate}, + * {@link IViewRule#onChildInserted} when a new child has been created/pasted/moved, and + * is inserted into a given parent. The parent may be null (for example when rendering + * top level items for preview). + * + * @param editor the XML editor to apply edits to the model for (performed by view + * rules) + * @param parentNode the parent XML node, or null if unknown + * @param childNode the XML node of the new node, never null + * @param overrideInsertType If not null, specifies an explicit insert type to use for + * edits made during the customization + */ + public void callCreateHooks( + AndroidXmlEditor editor, + NodeProxy parentNode, NodeProxy childNode, + InsertType overrideInsertType) { + IViewRule parentRule = null; + + if (parentNode != null) { + UiViewElementNode parentUiNode = parentNode.getNode(); + parentRule = loadRule(parentUiNode); + } + + if (overrideInsertType != null) { + mInsertType = overrideInsertType; + } + + UiViewElementNode newUiNode = childNode.getNode(); + IViewRule childRule = loadRule(newUiNode); + if (childRule != null || parentRule != null) { + callCreateHooks(editor, mInsertType, parentRule, parentNode, + childRule, childNode); + } + } + + private static void callCreateHooks( + final AndroidXmlEditor editor, final InsertType insertType, + final IViewRule parentRule, final INode parentNode, + final IViewRule childRule, final INode newNode) { + // Notify the parent about the new child in case it wants to customize it + // (For example, a ScrollView parent can go and set all its children's layout params to + // fill the parent.) + if (!editor.isEditXmlModelPending()) { + editor.wrapEditXmlModel(new Runnable() { + @Override + public void run() { + callCreateHooks(editor, insertType, + parentRule, parentNode, childRule, newNode); + } + }); + return; + } + + if (parentRule != null) { + parentRule.onChildInserted(newNode, parentNode, insertType); + } + + // Look up corresponding IViewRule, and notify the rule about + // this create action in case it wants to customize the new object. + // (For example, a rule for TabHosts can go and create a default child tab + // when you create it.) + if (childRule != null) { + childRule.onCreate(newNode, parentNode, insertType); + } + + if (parentNode != null) { + ((NodeProxy) parentNode).applyPendingChanges(); + } + } + + /** + * Set the type of insert currently in progress + * + * @param insertType the insert type to use for the next operation + */ + public void setInsertType(InsertType insertType) { + mInsertType = insertType; + } + + /** + * Return the type of insert currently in progress + * + * @return the type of insert currently in progress + */ + public InsertType getInsertType() { + return mInsertType; + } + + // ---- Deletion ---- + + public void callOnRemovingChildren(NodeProxy parentNode, + List<INode> children) { + if (parentNode != null) { + UiViewElementNode parentUiNode = parentNode.getNode(); + IViewRule parentRule = loadRule(parentUiNode); + if (parentRule != null) { + try { + parentRule.onRemovingChildren(children, parentNode, + mInsertType == InsertType.MOVE_WITHIN); + } catch (Exception e) { + AdtPlugin.log(e, "%s.onDispose() failed: %s", + parentRule.getClass().getSimpleName(), + e.toString()); + } + } + } + } + + // ---- private --- + + /** + * Returns the descriptor for the base View class. + * This could be null if the SDK or the given platform target hasn't loaded yet. + */ + private ViewElementDescriptor getBaseViewDescriptor() { + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + IAndroidTarget target = currentSdk.getTarget(mProject); + if (target != null) { + AndroidTargetData data = currentSdk.getTargetData(target); + return data.getLayoutDescriptors().getBaseViewDescriptor(); + } + } + return null; + } + + /** + * Clear the Rules cache. Calls onDispose() on each rule. + */ + private void clearCache() { + // The cache can contain multiple times the same rule instance for different + // keys (e.g. the UiViewElementNode key vs. the FQCN string key.) So transfer + // all values to a unique set. + HashSet<IViewRule> rules = new HashSet<IViewRule>(mRulesCache.values()); + + mRulesCache.clear(); + + for (IViewRule rule : rules) { + if (rule != null) { + try { + rule.onDispose(); + } catch (Exception e) { + AdtPlugin.log(e, "%s.onDispose() failed: %s", + rule.getClass().getSimpleName(), + e.toString()); + } + } + } + } + + /** + * Checks whether the project class loader has changed, and if so + * unregisters any view rules that use classes from the old class loader. It + * then returns the class loader to be used. + */ + private ClassLoader updateClassLoader() { + ClassLoader classLoader = mRuleLoader.getClassLoader(); + if (mUserClassLoader != null && classLoader != mUserClassLoader) { + // We have to unload all the IViewRules from the old class + List<Object> dispose = new ArrayList<Object>(); + for (Map.Entry<Object, IViewRule> entry : mRulesCache.entrySet()) { + IViewRule rule = entry.getValue(); + if (rule.getClass().getClassLoader() == mUserClassLoader) { + dispose.add(entry.getKey()); + } + } + for (Object object : dispose) { + mRulesCache.remove(object); + } + } + + mUserClassLoader = classLoader; + return mUserClassLoader; + } + + /** + * Load a rule using its descriptor. This will try to first load the rule using its + * actual FQCN and if that fails will find the first parent that works in the view + * hierarchy. + */ + private IViewRule loadRule(UiViewElementNode element) { + if (element == null) { + return null; + } + + String targetFqcn = null; + ViewElementDescriptor targetDesc = null; + + ElementDescriptor d = element.getDescriptor(); + if (d instanceof ViewElementDescriptor) { + targetDesc = (ViewElementDescriptor) d; + } + if (d == null || !(d instanceof ViewElementDescriptor)) { + // This should not happen. All views should have some kind of *view* element + // descriptor. Maybe the project is not complete and doesn't build or something. + // In this case, we'll use the descriptor of the base android View class. + targetDesc = getBaseViewDescriptor(); + } + + // Check whether any of the custom view .jar files have changed and if so + // unregister previously cached view rules to force a new view rule to be loaded. + updateClassLoader(); + + // Return the rule if we find it in the cache, even if it was stored as null + // (which means we didn't find it earlier, so don't look for it again) + IViewRule rule = mRulesCache.get(targetDesc); + if (rule != null || mRulesCache.containsKey(targetDesc)) { + return rule; + } + + // Get the descriptor and loop through the super class hierarchy + for (ViewElementDescriptor desc = targetDesc; + desc != null; + desc = desc.getSuperClassDesc()) { + + // Get the FQCN of this View + String fqcn = desc.getFullClassName(); + if (fqcn == null) { + // Shouldn't be happening. + return null; + } + + // The first time we keep the FQCN around as it's the target class we were + // initially trying to load. After, as we move through the hierarchy, the + // target FQCN remains constant. + if (targetFqcn == null) { + targetFqcn = fqcn; + } + + if (fqcn.indexOf('.') == -1) { + // Deal with unknown descriptors; these lack the full qualified path and + // elements in the layout without a package are taken to be in the + // android.widget package. + fqcn = ANDROID_WIDGET_PREFIX + fqcn; + } + + // Try to find a rule matching the "real" FQCN. If we find it, we're done. + // If not, the for loop will move to the parent descriptor. + rule = loadRule(fqcn, targetFqcn); + if (rule != null) { + // We found one. + // As a side effect, loadRule() also cached the rule using the target FQCN. + return rule; + } + } + + // Memorize in the cache that we couldn't find a rule for this descriptor + mRulesCache.put(targetDesc, null); + return null; + } + + /** + * Try to load a rule given a specific FQCN. This looks for an exact match in either + * the ADT scripts or the project scripts and does not look at parent hierarchy. + * <p/> + * Once a rule is found (or not), it is stored in a cache using its target FQCN + * so we don't try to reload it. + * <p/> + * The real FQCN is the actual rule class we're loading, e.g. "android.view.View" + * where target FQCN is the class we were initially looking for, which might be the same as + * the real FQCN or might be a derived class, e.g. "android.widget.TextView". + * + * @param realFqcn The FQCN of the rule class actually being loaded. + * @param targetFqcn The FQCN of the class actually processed, which might be different from + * the FQCN of the rule being loaded. + */ + IViewRule loadRule(String realFqcn, String targetFqcn) { + if (realFqcn == null || targetFqcn == null) { + return null; + } + + // Return the rule if we find it in the cache, even if it was stored as null + // (which means we didn't find it earlier, so don't look for it again) + IViewRule rule = mRulesCache.get(realFqcn); + if (rule != null || mRulesCache.containsKey(realFqcn)) { + return rule; + } + + // Look for class via reflection + try { + // For now, we package view rules for the builtin Android views and + // widgets with the tool in a special package, so look there rather + // than in the same package as the widgets. + String ruleClassName; + ClassLoader classLoader; + if (realFqcn.startsWith("android.") || //$NON-NLS-1$ + realFqcn.equals(VIEW_MERGE) || + realFqcn.endsWith(".GridLayout") || //$NON-NLS-1$ // Temporary special case + // FIXME: Remove this special case as soon as we pull + // the MapViewRule out of this code base and bundle it + // with the add ons + realFqcn.startsWith("com.google.android.maps.")) { //$NON-NLS-1$ + // This doesn't handle a case where there are name conflicts + // (e.g. where there are multiple different views with the same + // class name and only differing in package names, but that's a + // really bad practice in the first place, and if that situation + // should come up in the API we can enhance this algorithm. + String packageName = ViewRule.class.getName(); + packageName = packageName.substring(0, packageName.lastIndexOf('.')); + classLoader = RulesEngine.class.getClassLoader(); + int dotIndex = realFqcn.lastIndexOf('.'); + String baseName = realFqcn.substring(dotIndex+1); + // Capitalize rule class name to match naming conventions, if necessary (<merge>) + if (Character.isLowerCase(baseName.charAt(0))) { + if (baseName.equals(VIEW_TAG)) { + // Hack: ViewRule is generic for the "View" class, so we can't use it + // for the special XML "view" tag (lowercase); instead, the rule is + // named "ViewTagRule" instead. + baseName = "ViewTag"; //$NON-NLS-1$ + } + baseName = Character.toUpperCase(baseName.charAt(0)) + baseName.substring(1); + } + ruleClassName = packageName + "." + //$NON-NLS-1$ + baseName + "Rule"; //$NON-NLS-1$ + } else { + // Initialize the user-classpath for 3rd party IViewRules, if necessary + classLoader = updateClassLoader(); + if (classLoader == null) { + // The mUserClassLoader can be null; this is the typical scenario, + // when the user is only using builtin layout rules. + // This means however we can't resolve this fqcn since it's not + // in the name space of the builtin rules. + mRulesCache.put(realFqcn, null); + return null; + } + + // For other (3rd party) widgets, look in the same package (though most + // likely not in the same jar!) + ruleClassName = realFqcn + "Rule"; //$NON-NLS-1$ + } + + Class<?> clz = Class.forName(ruleClassName, true, classLoader); + rule = (IViewRule) clz.newInstance(); + return initializeRule(rule, targetFqcn); + } catch (ClassNotFoundException ex) { + // Not an unexpected error - this means that there isn't a helper for this + // class. + } catch (InstantiationException e) { + // This is NOT an expected error: fail. + AdtPlugin.log(e, "load rule error (%s): %s", realFqcn, e.toString()); + } catch (IllegalAccessException e) { + // This is NOT an expected error: fail. + AdtPlugin.log(e, "load rule error (%s): %s", realFqcn, e.toString()); + } + + // Memorize in the cache that we couldn't find a rule for this real FQCN + mRulesCache.put(realFqcn, null); + return null; + } + + /** + * Initialize a rule we just loaded. The rule has a chance to examine the target FQCN + * and bail out. + * <p/> + * Contract: the rule is not in the {@link #mRulesCache} yet and this method will + * cache it using the target FQCN if the rule is accepted. + * <p/> + * The real FQCN is the actual rule class we're loading, e.g. "android.view.View" + * where target FQCN is the class we were initially looking for, which might be the same as + * the real FQCN or might be a derived class, e.g. "android.widget.TextView". + * + * @param rule A rule freshly loaded. + * @param targetFqcn The FQCN of the class actually processed, which might be different from + * the FQCN of the rule being loaded. + * @return The rule if accepted, or null if the rule can't handle that FQCN. + */ + private IViewRule initializeRule(IViewRule rule, String targetFqcn) { + + try { + if (rule.onInitialize(targetFqcn, new ClientRulesEngine(this, targetFqcn))) { + // Add it to the cache and return it + mRulesCache.put(targetFqcn, rule); + return rule; + } else { + rule.onDispose(); + } + } catch (Exception e) { + AdtPlugin.log(e, "%s.onInit() failed: %s", + rule.getClass().getSimpleName(), + e.toString()); + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java new file mode 100644 index 000000000..5f2659ef2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java @@ -0,0 +1,856 @@ +/* + * 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.layout.gre; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_ID; +import static com.android.SdkConstants.FQCN_BUTTON; +import static com.android.SdkConstants.FQCN_SPINNER; +import static com.android.SdkConstants.FQCN_TOGGLE_BUTTON; +import static com.android.SdkConstants.ID_PREFIX; +import static com.android.SdkConstants.NEW_ID_PREFIX; +import static com.android.SdkConstants.VIEW_FRAGMENT; +import static com.android.SdkConstants.VIEW_INCLUDE; + +import com.android.annotations.VisibleForTesting; +import com.android.ide.common.api.IViewMetadata.FillPreference; +import com.android.ide.common.api.Margins; +import com.android.ide.common.api.ResizePolicy; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; +import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.resources.Density; +import com.android.utils.Pair; +import com.google.common.base.Splitter; +import com.google.common.io.Closeables; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +/** + * The {@link ViewMetadataRepository} contains additional metadata for Android view + * classes + */ +public class ViewMetadataRepository { + private static final String PREVIEW_CONFIG_FILENAME = "rendering-configs.xml"; //$NON-NLS-1$ + private static final String METADATA_FILENAME = "extra-view-metadata.xml"; //$NON-NLS-1$ + + /** Singleton instance */ + private static ViewMetadataRepository sInstance = new ViewMetadataRepository(); + + /** + * Returns the singleton instance + * + * @return the {@link ViewMetadataRepository} + */ + public static ViewMetadataRepository get() { + return sInstance; + } + + /** + * Ever increasing counter used to assign natural ordering numbers to views and + * categories + */ + private static int sNextOrdinal = 0; + + /** + * List of categories (which contain views); constructed lazily so use + * {@link #getCategories()} + */ + private List<CategoryData> mCategories; + + /** + * Map from class names to view data objects; constructed lazily so use + * {@link #getClassToView} + */ + private Map<String, ViewData> mClassToView; + + /** Hidden constructor: Create via factory {@link #get()} instead */ + private ViewMetadataRepository() { + } + + /** Returns a map from class fully qualified names to {@link ViewData} objects */ + private Map<String, ViewData> getClassToView() { + if (mClassToView == null) { + int initialSize = 75; + mClassToView = new HashMap<String, ViewData>(initialSize); + List<CategoryData> categories = getCategories(); + for (CategoryData category : categories) { + for (ViewData view : category) { + mClassToView.put(view.getFcqn(), view); + } + } + assert mClassToView.size() <= initialSize; + } + + return mClassToView; + } + + /** + * Returns an XML document containing rendering configurations for the various Android + * views. The FQN of each view can be obtained via the + * {@link #getFullClassName(Element)} method + * + * @return an XML document containing rendering elements + */ + public Document getRenderingConfigDoc() { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + Class<ViewMetadataRepository> clz = ViewMetadataRepository.class; + InputStream paletteStream = clz.getResourceAsStream(PREVIEW_CONFIG_FILENAME); + InputSource is = new InputSource(paletteStream); + try { + factory.setNamespaceAware(true); + factory.setValidating(false); + factory.setIgnoringComments(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(is); + } catch (Exception e) { + AdtPlugin.log(e, "Parsing palette file failed"); + return null; + } finally { + Closeables.closeQuietly(paletteStream); + } + } + + /** + * Returns a fully qualified class name for an element in the rendering document + * returned by {@link #getRenderingConfigDoc()} + * + * @param element the element to look up the fqcn for + * @return the fqcn of the view the element represents a preview for + */ + public String getFullClassName(Element element) { + // We don't use the element tag name, because in some cases we have + // an outer element to render some interesting inner element, such as a tab widget + // (which must be rendered inside a tab host). + // + // Therefore, we instead use the convention that the id is the fully qualified + // class name, with .'s replaced with _'s. + + // Special case: for tab host we aren't allowed to mess with the id + String id = element.getAttributeNS(ANDROID_URI, ATTR_ID); + + if ("@android:id/tabhost".equals(id)) { + // Special case to distinguish TabHost and TabWidget + NodeList children = element.getChildNodes(); + if (children.getLength() > 1 && (children.item(1) instanceof Element)) { + Element child = (Element) children.item(1); + String childId = child.getAttributeNS(ANDROID_URI, ATTR_ID); + if ("@+id/android_widget_TabWidget".equals(childId)) { + return "android.widget.TabWidget"; // TODO: Tab widget! + } + } + return "android.widget.TabHost"; // TODO: Tab widget! + } + + StringBuilder sb = new StringBuilder(); + int i = 0; + if (id.startsWith(NEW_ID_PREFIX)) { + i = NEW_ID_PREFIX.length(); + } else if (id.startsWith(ID_PREFIX)) { + i = ID_PREFIX.length(); + } + + for (; i < id.length(); i++) { + char c = id.charAt(i); + if (c == '_') { + sb.append('.'); + } else { + sb.append(c); + } + } + + return sb.toString(); + } + + /** Returns an ordered list of categories and views, parsed from a metadata file */ + @SuppressWarnings("resource") // streams passed to parser InputSource closed by parser + private List<CategoryData> getCategories() { + if (mCategories == null) { + mCategories = new ArrayList<CategoryData>(); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + Class<ViewMetadataRepository> clz = ViewMetadataRepository.class; + InputStream inputStream = clz.getResourceAsStream(METADATA_FILENAME); + InputSource is = new InputSource(new BufferedInputStream(inputStream)); + try { + factory.setNamespaceAware(true); + factory.setValidating(false); + factory.setIgnoringComments(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(is); + Map<String, FillPreference> fillTypes = new HashMap<String, FillPreference>(); + for (FillPreference pref : FillPreference.values()) { + fillTypes.put(pref.toString().toLowerCase(Locale.US), pref); + } + + NodeList categoryNodes = document.getDocumentElement().getChildNodes(); + for (int i = 0, n = categoryNodes.getLength(); i < n; i++) { + Node node = categoryNodes.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) node; + if (element.getNodeName().equals("category")) { //$NON-NLS-1$ + String name = element.getAttribute("name"); //$NON-NLS-1$ + CategoryData category = new CategoryData(name); + NodeList children = element.getChildNodes(); + for (int j = 0, m = children.getLength(); j < m; j++) { + Node childNode = children.item(j); + if (childNode.getNodeType() == Node.ELEMENT_NODE) { + Element child = (Element) childNode; + ViewData view = createViewData(fillTypes, child, + null, FillPreference.NONE, RenderMode.NORMAL, null); + category.addView(view); + } + } + mCategories.add(category); + } + } + } + } catch (Exception e) { + AdtPlugin.log(e, "Invalid palette metadata"); //$NON-NLS-1$ + } + } + + return mCategories; + } + + private ViewData createViewData(Map<String, FillPreference> fillTypes, + Element child, String defaultFqcn, FillPreference defaultFill, + RenderMode defaultRender, String defaultSize) { + String fqcn = child.getAttribute("class"); //$NON-NLS-1$ + if (fqcn.length() == 0) { + fqcn = defaultFqcn; + } + String fill = child.getAttribute("fill"); //$NON-NLS-1$ + FillPreference fillPreference = null; + if (fill.length() > 0) { + fillPreference = fillTypes.get(fill); + } + if (fillPreference == null) { + fillPreference = defaultFill; + } + String skip = child.getAttribute("skip"); //$NON-NLS-1$ + RenderMode renderMode = defaultRender; + String render = child.getAttribute("render"); //$NON-NLS-1$ + if (render.length() > 0) { + renderMode = RenderMode.get(render); + } + String displayName = child.getAttribute("name"); //$NON-NLS-1$ + if (displayName.length() == 0) { + displayName = null; + } + + String relatedTo = child.getAttribute("relatedTo"); //$NON-NLS-1$ + String topAttrs = child.getAttribute("topAttrs"); //$NON-NLS-1$ + String resize = child.getAttribute("resize"); //$NON-NLS-1$ + ViewData view = new ViewData(fqcn, displayName, fillPreference, + skip.length() == 0 ? false : Boolean.valueOf(skip), + renderMode, relatedTo, resize, topAttrs); + + String init = child.getAttribute("init"); //$NON-NLS-1$ + String icon = child.getAttribute("icon"); //$NON-NLS-1$ + + view.setInitString(init); + if (icon.length() > 0) { + view.setIconName(icon); + } + + // Nested variations? + if (child.hasChildNodes()) { + // Palette variations + NodeList childNodes = child.getChildNodes(); + for (int k = 0, kl = childNodes.getLength(); k < kl; k++) { + Node variationNode = childNodes.item(k); + if (variationNode.getNodeType() == Node.ELEMENT_NODE) { + Element variation = (Element) variationNode; + ViewData variationView = createViewData(fillTypes, variation, + fqcn, fillPreference, renderMode, resize); + view.addVariation(variationView); + } + } + } + + return view; + } + + /** + * Computes the palette entries for the given {@link AndroidTargetData}, looking up the + * available node descriptors, categorizing and sorting them. + * + * @param targetData the target data for which to compute palette entries + * @param alphabetical if true, sort all items in alphabetical order + * @param createCategories if true, organize the items into categories + * @return a list of pairs where each pair contains of the category label and an + * ordered list of elements to be included in that category + */ + public List<Pair<String, List<ViewElementDescriptor>>> getPaletteEntries( + AndroidTargetData targetData, boolean alphabetical, boolean createCategories) { + List<Pair<String, List<ViewElementDescriptor>>> result = + new ArrayList<Pair<String, List<ViewElementDescriptor>>>(); + + List<List<ViewElementDescriptor>> lists = new ArrayList<List<ViewElementDescriptor>>(2); + LayoutDescriptors layoutDescriptors = targetData.getLayoutDescriptors(); + lists.add(layoutDescriptors.getViewDescriptors()); + lists.add(layoutDescriptors.getLayoutDescriptors()); + + // First record map of FQCN to ViewElementDescriptor such that we can quickly + // determine if a particular palette entry is available + Map<String, ViewElementDescriptor> fqcnToDescriptor = + new HashMap<String, ViewElementDescriptor>(); + for (List<ViewElementDescriptor> list : lists) { + for (ViewElementDescriptor view : list) { + String fqcn = view.getFullClassName(); + if (fqcn == null) { + // <view> and <merge> tags etc + fqcn = view.getUiName(); + } + fqcnToDescriptor.put(fqcn, view); + } + } + + Set<ViewElementDescriptor> remaining = new HashSet<ViewElementDescriptor>( + layoutDescriptors.getViewDescriptors().size() + + layoutDescriptors.getLayoutDescriptors().size()); + remaining.addAll(layoutDescriptors.getViewDescriptors()); + remaining.addAll(layoutDescriptors.getLayoutDescriptors()); + + // Now iterate in palette metadata order over the items in the palette and include + // any that also appear as a descriptor + List<ViewElementDescriptor> categoryItems = new ArrayList<ViewElementDescriptor>(); + for (CategoryData category : getCategories()) { + if (createCategories) { + categoryItems = new ArrayList<ViewElementDescriptor>(); + } + for (ViewData view : category) { + String fqcn = view.getFcqn(); + ViewElementDescriptor descriptor = fqcnToDescriptor.get(fqcn); + if (descriptor != null) { + remaining.remove(descriptor); + if (view.getSkip()) { + continue; + } + + if (view.getDisplayName() != null || view.getInitString().length() > 0) { + categoryItems.add(new PaletteMetadataDescriptor(descriptor, + view.getDisplayName(), view.getInitString(), view.getIconName())); + } else { + categoryItems.add(descriptor); + } + + if (view.hasVariations()) { + for (ViewData variation : view.getVariations()) { + String init = variation.getInitString(); + String icon = variation.getIconName(); + ViewElementDescriptor desc = new PaletteMetadataDescriptor(descriptor, + variation.getDisplayName(), init, icon); + categoryItems.add(desc); + } + } + } + } + + if (createCategories && categoryItems.size() > 0) { + if (alphabetical) { + Collections.sort(categoryItems); + } + result.add(Pair.of(category.getName(), categoryItems)); + } + } + + if (remaining.size() > 0) { + List<ViewElementDescriptor> otherItems = + new ArrayList<ViewElementDescriptor>(remaining); + // Always sorted, we don't have a natural order for these unknowns + Collections.sort(otherItems); + if (createCategories) { + result.add(Pair.of("Other", otherItems)); + } else { + categoryItems.addAll(otherItems); + } + } + + if (!createCategories) { + if (alphabetical) { + Collections.sort(categoryItems); + } + result.add(Pair.of("Views", categoryItems)); + } + + return result; + } + + @VisibleForTesting + Collection<String> getAllFqcns() { + return getClassToView().keySet(); + } + + /** + * Metadata holder for a particular category - contains the name of the category, its + * ordinal (for natural/logical sorting order) and views contained in the category + */ + private static class CategoryData implements Iterable<ViewData>, Comparable<CategoryData> { + /** Category name */ + private final String mName; + /** Views included in this category */ + private final List<ViewData> mViews = new ArrayList<ViewData>(); + /** Natural ordering rank */ + private final int mOrdinal = sNextOrdinal++; + + /** Constructs a new category with the given name */ + private CategoryData(String name) { + super(); + mName = name; + } + + /** Adds a new view into this category */ + private void addView(ViewData view) { + mViews.add(view); + } + + private String getName() { + return mName; + } + + // Implements Iterable<ViewData> such that we can use for-each on the category to + // enumerate its views + @Override + public Iterator<ViewData> iterator() { + return mViews.iterator(); + } + + // Implements Comparable<CategoryData> such that categories can be naturally sorted + @Override + public int compareTo(CategoryData other) { + return mOrdinal - other.mOrdinal; + } + } + + /** Metadata holder for a view of a given fully qualified class name */ + private static class ViewData implements Comparable<ViewData> { + /** The fully qualified class name of the view */ + private final String mFqcn; + /** Fill preference of the view */ + private final FillPreference mFillPreference; + /** Skip this item in the palette? */ + private final boolean mSkip; + /** Must this item be rendered alone? skipped? etc */ + private final RenderMode mRenderMode; + /** Related views */ + private final String mRelatedTo; + /** The relative rank of the view for natural ordering */ + private final int mOrdinal = sNextOrdinal++; + /** List of optional variations */ + private List<ViewData> mVariations; + /** Display name. Can be null. */ + private String mDisplayName; + /** + * Optional initialization string - a comma separate set of name/value pairs to + * initialize the element with + */ + private String mInitString; + /** The name of an icon (known to the {@link IconFactory} to show for this view */ + private String mIconName; + /** The resize preference of this view */ + private String mResize; + /** The most commonly set attributes of this view */ + private String mTopAttrs; + + /** Constructs a new view data for the given class */ + private ViewData(String fqcn, String displayName, + FillPreference fillPreference, boolean skip, RenderMode renderMode, + String relatedTo, String resize, String topAttrs) { + super(); + mFqcn = fqcn; + mDisplayName = displayName; + mFillPreference = fillPreference; + mSkip = skip; + mRenderMode = renderMode; + mRelatedTo = relatedTo; + mResize = resize; + mTopAttrs = topAttrs; + } + + /** Returns the {@link FillPreference} for views of this type */ + private FillPreference getFillPreference() { + return mFillPreference; + } + + /** Fully qualified class name of views of this type */ + private String getFcqn() { + return mFqcn; + } + + private String getDisplayName() { + return mDisplayName; + } + + private String getResize() { + return mResize; + } + + // Implements Comparable<ViewData> such that views can be sorted naturally + @Override + public int compareTo(ViewData other) { + return mOrdinal - other.mOrdinal; + } + + public RenderMode getRenderMode() { + return mRenderMode; + } + + public boolean getSkip() { + return mSkip; + } + + public List<String> getRelatedTo() { + if (mRelatedTo == null || mRelatedTo.length() == 0) { + return Collections.emptyList(); + } else { + List<String> result = new ArrayList<String>(); + ViewMetadataRepository repository = ViewMetadataRepository.get(); + Map<String, ViewData> classToView = repository.getClassToView(); + + List<String> fqns = new ArrayList<String>(classToView.keySet()); + for (String basename : Splitter.on(',').split(mRelatedTo)) { + boolean found = false; + for (String fqcn : fqns) { + String suffix = '.' + basename; + if (fqcn.endsWith(suffix)) { + result.add(fqcn); + found = true; + break; + } + } + if (basename.equals(VIEW_FRAGMENT) || basename.equals(VIEW_INCLUDE)) { + result.add(basename); + } else { + assert found : basename; + } + } + + return result; + } + } + + public List<String> getTopAttributes() { + // "id" is a top attribute for all views, so it is not included in the XML, we just + // add it in dynamically here + if (mTopAttrs == null || mTopAttrs.length() == 0) { + return Collections.singletonList(ATTR_ID); + } else { + String[] split = mTopAttrs.split(","); //$NON-NLS-1$ + List<String> topAttributes = new ArrayList<String>(split.length + 1); + topAttributes.add(ATTR_ID); + for (int i = 0, n = split.length; i < n; i++) { + topAttributes.add(split[i]); + } + return Collections.<String>unmodifiableList(topAttributes); + } + } + + void addVariation(ViewData variation) { + if (mVariations == null) { + mVariations = new ArrayList<ViewData>(4); + } + mVariations.add(variation); + } + + List<ViewData> getVariations() { + return mVariations; + } + + boolean hasVariations() { + return mVariations != null && mVariations.size() > 0; + } + + private void setInitString(String initString) { + this.mInitString = initString; + } + + private String getInitString() { + return mInitString; + } + + private void setIconName(String iconName) { + this.mIconName = iconName; + } + + private String getIconName() { + return mIconName; + } + } + + /** + * Returns the {@link FillPreference} for classes with the given fully qualified class + * name + * + * @param fqcn the fully qualified class name of the view + * @return a suitable {@link FillPreference} for the given view type + */ + public FillPreference getFillPreference(String fqcn) { + ViewData view = getClassToView().get(fqcn); + if (view != null) { + return view.getFillPreference(); + } + + return FillPreference.NONE; + } + + /** + * Returns the {@link RenderMode} for classes with the given fully qualified class + * name + * + * @param fqcn the fully qualified class name + * @return the {@link RenderMode} to use for previews of the given view type + */ + public RenderMode getRenderMode(String fqcn) { + ViewData view = getClassToView().get(fqcn); + if (view != null) { + return view.getRenderMode(); + } + + return RenderMode.NORMAL; + } + + /** + * Returns the {@link ResizePolicy} for the given class. + * + * @param fqcn the fully qualified class name of the target widget + * @return the {@link ResizePolicy} for the widget, which will never be null (but may + * be the default of {@link ResizePolicy#full()} if no metadata is found for + * the given widget) + */ + public ResizePolicy getResizePolicy(String fqcn) { + ViewData view = getClassToView().get(fqcn); + if (view != null) { + String resize = view.getResize(); + if (resize != null && resize.length() > 0) { + if ("full".equals(resize)) { //$NON-NLS-1$ + return ResizePolicy.full(); + } else if ("none".equals(resize)) { //$NON-NLS-1$ + return ResizePolicy.none(); + } else if ("horizontal".equals(resize)) { //$NON-NLS-1$ + return ResizePolicy.horizontal(); + } else if ("vertical".equals(resize)) { //$NON-NLS-1$ + return ResizePolicy.vertical(); + } else if ("scaled".equals(resize)) { //$NON-NLS-1$ + return ResizePolicy.scaled(); + } else { + assert false : resize; + } + } + } + + return ResizePolicy.full(); + } + + /** + * Returns true if classes with the given fully qualified class name should be hidden + * or skipped from the palette + * + * @param fqcn the fully qualified class name + * @return true if views of the given type should be hidden from the palette + */ + public boolean getSkip(String fqcn) { + ViewData view = getClassToView().get(fqcn); + if (view != null) { + return view.getSkip(); + } + + return false; + } + + /** + * Returns a list of the top (most commonly set) attributes of the given + * view. + * + * @param fqcn the fully qualified class name + * @return a list, never null but possibly empty, of popular attribute names + * (not including a namespace prefix) + */ + public List<String> getTopAttributes(String fqcn) { + ViewData view = getClassToView().get(fqcn); + if (view != null) { + return view.getTopAttributes(); + } + + return Collections.singletonList(ATTR_ID); + } + + /** + * Returns a set of fully qualified names for views that are closely related to the + * given view + * + * @param fqcn the fully qualified class name + * @return a list, never null but possibly empty, of views that are related to the + * view of the given type + */ + public List<String> getRelatedTo(String fqcn) { + ViewData view = getClassToView().get(fqcn); + if (view != null) { + return view.getRelatedTo(); + } + + return Collections.emptyList(); + } + + /** Render mode for palette preview */ + public enum RenderMode { + /** + * Render previews, and it can be rendered as a sibling of many other views in a + * big linear layout + */ + NORMAL, + /** This view needs to be rendered alone */ + ALONE, + /** + * Skip this element; it doesn't work or does not produce any visible artifacts + * (such as the basic layouts) + */ + SKIP; + + /** + * Returns the {@link RenderMode} for the given render XML attribute + * value + * + * @param render the attribute value in the metadata XML file + * @return a corresponding {@link RenderMode}, never null + */ + public static RenderMode get(String render) { + if ("alone".equals(render)) { //$NON-NLS-1$ + return ALONE; + } else if ("skip".equals(render)) { //$NON-NLS-1$ + return SKIP; + } else { + return NORMAL; + } + } + } + + /** + * Are insets supported yet? This flag indicates whether the {@link #getInsets} method + * can return valid data, such that clients can avoid doing any work computing the + * current theme or density if there's no chance that valid insets will be returned + */ + public static final boolean INSETS_SUPPORTED = false; + + /** + * Returns the insets of widgets with the given fully qualified name, in the given + * theme and the given screen density. + * + * @param fqcn the fully qualified name of the view + * @param density the screen density + * @param theme the theme name + * @return the insets of the visual bounds relative to the view info bounds, or null + * if not known or if there are no insets + */ + public static Margins getInsets(String fqcn, Density density, String theme) { + if (INSETS_SUPPORTED) { + // Some sample data measured manually for common themes and widgets. + if (fqcn.equals(FQCN_BUTTON)) { + if (density == Density.HIGH) { + if (theme.startsWith(HOLO_PREFIX)) { + // Theme.Holo, Theme.Holo.Light, WVGA + return new Margins(5, 5, 5, 5); + } else { + // Theme.Light, WVGA + return new Margins(4, 4, 0, 7); + } + } else if (density == Density.MEDIUM) { + if (theme.startsWith(HOLO_PREFIX)) { + // Theme.Holo, Theme.Holo.Light, WVGA + return new Margins(3, 3, 3, 3); + } else { + // Theme.Light, HVGA + return new Margins(2, 2, 0, 4); + } + } else if (density == Density.LOW) { + if (theme.startsWith(HOLO_PREFIX)) { + // Theme.Holo, Theme.Holo.Light, QVGA + return new Margins(2, 2, 2, 2); + } else { + // Theme.Light, QVGA + return new Margins(1, 3, 0, 4); + } + } + } else if (fqcn.equals(FQCN_TOGGLE_BUTTON)) { + if (density == Density.HIGH) { + if (theme.startsWith(HOLO_PREFIX)) { + // Theme.Holo, Theme.Holo.Light, WVGA + return new Margins(5, 5, 5, 5); + } else { + // Theme.Light, WVGA + return new Margins(2, 2, 0, 5); + } + } else if (density == Density.MEDIUM) { + if (theme.startsWith(HOLO_PREFIX)) { + // Theme.Holo, Theme.Holo.Light, WVGA + return new Margins(3, 3, 3, 3); + } else { + // Theme.Light, HVGA + return new Margins(0, 1, 0, 3); + } + } else if (density == Density.LOW) { + if (theme.startsWith(HOLO_PREFIX)) { + // Theme.Holo, Theme.Holo.Light, QVGA + return new Margins(2, 2, 2, 2); + } else { + // Theme.Light, QVGA + return new Margins(2, 2, 0, 4); + } + } + } else if (fqcn.equals(FQCN_SPINNER)) { + if (density == Density.HIGH) { + if (!theme.startsWith(HOLO_PREFIX)) { + // Theme.Light, WVGA + return new Margins(3, 4, 2, 8); + } // Doesn't render on Holo! + } else if (density == Density.MEDIUM) { + if (!theme.startsWith(HOLO_PREFIX)) { + // Theme.Light, HVGA + return new Margins(1, 1, 0, 4); + } + } + } + } + + return null; + } + + private static final String HOLO_PREFIX = "Theme.Holo"; //$NON-NLS-1$ +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml new file mode 100644 index 000000000..6a67b1db4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml @@ -0,0 +1,452 @@ +<?xml version="1.0" encoding="UTF-8"?> + <!-- + Palette Metadata + + This document provides additional designtime metadata for various Android views, such as + logical palette categories (as well as a natural ordering of the views within their + categories, fill-preferences (how a view will sets its width and height attributes when + dropped into other views), and so on. + --> +<!DOCTYPE metadata [ +<!--- The metadata consists of a series of category definitions --> +<!ELEMENT metadata (category)*> +<!--- Each category has a name and contains a list of views in order --> +<!ELEMENT category (view)*> +<!ATTLIST category name CDATA #IMPLIED> +<!--- Each view is identified by its full class name and has various + other attributes such as a fill preference --> +<!ELEMENT view (view)*> +<!ATTLIST view + class CDATA #IMPLIED + name CDATA #IMPLIED + init CDATA #IMPLIED + icon CDATA #IMPLIED + relatedTo CDATA #IMPLIED + skip (true|false) "false" + render (alone|skip|normal) "normal" + fill (none|both|width|height|opposite|width_in_vertical|height_in_horizontal) "none" + resize (full|none|horizontal|vertical|scaled) "full" + topAttrs CDATA #IMPLIED +> +]> +<metadata> + <category + name="Form Widgets"> + <view + class="android.widget.TextView" + topAttrs="text,textAppearance,textColor,textSize" + name="TextView" + init="" + relatedTo="EditText,AutoCompleteTextView,MultiAutoCompleteTextView"> + <view + name="Large Text" + init="android:textAppearance=?android:attr/textAppearanceLarge,android:text=Large Text" /> + <view + name="Medium Text" + init="android:textAppearance=?android:attr/textAppearanceMedium,android:text=Medium Text" /> + <view + name="Small Text" + init="android:textAppearance=?android:attr/textAppearanceSmall,android:text=Small Text" /> + </view> + <view + class="android.widget.Button" + topAttrs="text,style" + name="Button" + init="" + relatedTo="ImageButton"> + <view + name="Small Button" + init="style=?android:attr/buttonStyleSmall,android:text=Button" /> + </view> + <view + class="android.widget.ToggleButton" + topAttrs="textOff,textOn,style,background" + relatedTo="CheckBox" /> + <view + class="android.widget.CheckBox" + topAttrs="text" + relatedTo="RadioButton,ToggleButton,CheckedTextView" /> + <view + class="android.widget.RadioButton" + topAttrs="text,style" + relatedTo="CheckBox,ToggleButton" /> + <view + class="android.widget.CheckedTextView" + topAttrs="gravity,paddingLeft,paddingRight,checkMark,textAppearance" + relatedTo="TextView,CheckBox" /> + <view + class="android.widget.Spinner" + topAttrs="prompt,entries,style" + relatedTo="EditText" + fill="width_in_vertical" /> + <view + class="android.widget.ProgressBar" + topAttrs="style,visibility,indeterminate,max" + relatedTo="SeekBar" + name="ProgressBar (Large)" + init="style=?android:attr/progressBarStyleLarge" + resize="scaled" > + <view + name="ProgressBar (Normal)" + init="" + resize="scaled" /> + <view + name="ProgressBar (Small)" + init="style=?android:attr/progressBarStyleSmall" + resize="scaled" /> + <view + name="ProgressBar (Horizontal)" + init="style=?android:attr/progressBarStyleHorizontal" + resize="horizontal" /> + </view> + <view + class="android.widget.SeekBar" + topAttrs="paddingLeft,paddingRight,progressDrawable,thumb" + relatedTo="ProgressBar" + resize="horizontal" + fill="width_in_vertical" /> + <view + class="android.widget.QuickContactBadge" + topAttrs="src,style,gravity" + resize="scaled" /> + <view + class="android.widget.RadioGroup" + topAttrs="orientation,paddingBottom,paddingTop,style" /> + <view + class="android.widget.RatingBar" + topAttrs="numStars,stepSize,style,isIndicator" + resize="horizontal" /> + <view + class="android.widget.Switch" + topAttrs="text,textOff,textOn,style,checked" + relatedTo="CheckBox,ToggleButton" + render="alone" /> + </category> + <category + name="Text Fields"> + <view + class="android.widget.EditText" + topAttrs="hint,inputType,singleLine" + name="Plain Text" + init="" + resize="full" + relatedTo="Spinner,TextView,AutoCompleteTextView,MultiAutoCompleteTextView" + fill="width_in_vertical"> + <view + name="Person Name" + init="android:inputType=textPersonName" /> + <view + name="Password" + init="android:inputType=textPassword" /> + <view + name="Password (Numeric)" + init="android:inputType=numberPassword" /> + <view + name="E-mail" + init="android:inputType=textEmailAddress" /> + <view + name="Phone" + init="android:inputType=phone" /> + <view + name="Postal Address" + resize="full" + init="android:inputType=textPostalAddress" /> + <view + name="Multiline Text" + resize="full" + init="android:inputType=textMultiLine" /> + <view + name="Time" + init="android:inputType=time" /> + <view + name="Date" + init="android:inputType=date" /> + <view + name="Number" + init="android:inputType=number" /> + <view + name="Number (Signed)" + init="android:inputType=numberSigned" /> + <view + name="Number (Decimal)" + init="android:inputType=numberDecimal" /> + </view> + <view + class="android.widget.AutoCompleteTextView" + topAttrs="singleLine,autoText" + fill="width_in_vertical" /> + <view + class="android.widget.MultiAutoCompleteTextView" + topAttrs="background,hint,imeOptions,inputType,style,textColor" + fill="width_in_vertical" /> + </category> + <category + name="Layouts"> + <view + class="android.widget.GridLayout" + fill="opposite" + render="skip" /> + <view + class="android.widget.LinearLayout" + topAttrs="orientation,gravity" + name="LinearLayout (Vertical)" + init="android:orientation=vertical" + icon="VerticalLinearLayout" + fill="opposite" + render="skip"> + <view + name="LinearLayout (Horizontal)" /> + </view> + <view + class="android.widget.RelativeLayout" + topAttrs="background,orientation,paddingLeft" + fill="opposite" + render="skip" /> + <view + class="android.widget.FrameLayout" + topAttrs="background" + fill="opposite" + render="skip" /> + <view + class="include" + topAttrs="layout" + name="Include Other Layout" + render="skip" + relatedTo="fragment" /> + <view + class="fragment" + topAttrs="class,name" + name="Fragment" + fill="opposite" + render="skip" + relatedTo="include" /> + <view + class="android.widget.TableLayout" + topAttrs="stretchColumns,shrinkColumns,orientation" + fill="opposite" + render="skip" /> + <view + class="android.widget.TableRow" + topAttrs="paddingTop,focusable,gravity,visibility" + fill="opposite" + resize="vertical" + render="skip" /> + <view + class="android.widget.Space" + fill="opposite" + render="skip" /> + </category> + <category + name="Composite"> + <view + class="android.widget.ListView" + topAttrs="drawSelectorOnTop,cacheColorHint,divider,background" + relatedTo="ExpandableListView" + fill="width_in_vertical" /> + <view + class="android.widget.ExpandableListView" + topAttrs="drawSelectorOnTop,cacheColorHint,indicatorLeft,indicatorRight,scrollbars,textSize" + relatedTo="ListView" + fill="width_in_vertical" /> + <view + class="android.widget.GridView" + topAttrs="numColumns,verticalSpacing,horizontalSpacing" + fill="opposite" + render="skip" /> + <view + class="android.widget.ScrollView" + topAttrs="fillViewport,orientation,scrollbars" + relatedTo="HorizontalScrollView" + fill="opposite" + render="skip" /> + <view + class="android.widget.HorizontalScrollView" + topAttrs="scrollbars,fadingEdgeLength,fadingEdge" + relatedTo="ScrollView" + render="skip" /> + <view + class="android.widget.SearchView" + topAttrs="iconifiedByDefault,queryHint,maxWidth,minWidth,visibility" + render="skip" /> + <view + class="android.widget.SlidingDrawer" + render="skip" + topAttrs="allowSingleTap,bottomOffset,content,handle,topOffset,visibility" /> + <view + class="android.widget.TabHost" + topAttrs="paddingTop,background,duplicateParentState,visibility" + fill="width_in_vertical" + render="alone" /> + <view + class="android.widget.TabWidget" + topAttrs="background,paddingLeft,tabStripEnabled,gravity" + render="alone" /> + <view + class="android.webkit.WebView" + topAttrs="background,visibility,textAppearance" + fill="opposite" + render="skip" /> + </category> + <category + name="Images & Media"> + <view + class="android.widget.ImageView" + topAttrs="src,scaleType" + resize="scaled" + render="skip" + relatedTo="ImageButton,VideoView" /> + <view + class="android.widget.ImageButton" + topAttrs="src,background,style" + resize="scaled" + render="skip" + relatedTo="Button,ImageView" /> + <view + class="android.widget.Gallery" + topAttrs="gravity,spacing,background" + fill="width_in_vertical" + render="skip" /> + <view + class="android.widget.MediaController" + render="skip" /> + <view + class="android.widget.VideoView" + relatedTo="ImageView" + fill="opposite" + render="skip" /> + </category> + <category + name="Time & Date"> + <view + class="android.widget.TimePicker" + topAttrs="visibility" + relatedTo="DatePicker,CalendarView" + render="alone" /> + <view + class="android.widget.DatePicker" + relatedTo="TimePicker" + render="alone" /> + <view + class="android.widget.CalendarView" + topAttrs="focusable,focusableInTouchMode,visibility" + fill="both" + relatedTo="TimePicker,DatePicker" /> + <view + class="android.widget.Chronometer" + topAttrs="textSize,gravity,visibility" + render="skip" /> + <view + class="android.widget.AnalogClock" + topAttrs="dial,hand_hour,hand_minute" + relatedTo="DigitalClock" /> + <view + class="android.widget.DigitalClock" + relatedTo="AnalogClock" /> + </category> + <category + name="Transitions"> + <view + class="android.widget.ImageSwitcher" + topAttrs="inAnimation,outAnimation,cropToPadding,padding,scaleType" + relatedTo="ViewFlipper,ViewSwitcher,TextSwitcher" + render="skip" /> + <view + class="android.widget.AdapterViewFlipper" + topAttrs="autoStart,flipInterval,inAnimation,outAnimation" + fill="opposite" + render="skip" /> + <view + class="android.widget.StackView" + topAttrs="loopViews,gravity" + fill="opposite" + render="skip" /> + <view + class="android.widget.TextSwitcher" + relatedTo="ViewFlipper,ImageSwitcher,ViewSwitcher" + fill="opposite" + render="skip" /> + <view + class="android.widget.ViewAnimator" + topAttrs="inAnimation,outAnimation" + fill="opposite" + render="skip" /> + <view + class="android.widget.ViewFlipper" + topAttrs="flipInterval,inAnimation,outAnimation,addStatesFromChildren,measureAllChildren" + relatedTo="ViewSwitcher,ImageSwitcher,TextSwitcher" + fill="opposite" + render="skip" /> + <view + class="android.widget.ViewSwitcher" + topAttrs="inAnimation,outAnimation" + relatedTo="ViewFlipper,ImageSwitcher,TextSwitcher" + fill="opposite" + render="skip" /> + </category> + <category + name="Advanced"> + <view + class="requestFocus" + render="skip" /> + <view + class="android.view.View" + topAttrs="background,visibility,style" + render="skip" /> + <view + class="android.view.ViewStub" + topAttrs="layout,inflatedId,visibility" + render="skip" /> + <view + class="view" + topAttrs="class" + render="skip" /> + <view + class="android.gesture.GestureOverlayView" + topAttrs="gestureStrokeType,uncertainGestureColor,eventsInterceptionEnabled,gestureColor,orientation" + render="skip" /> + <view + class="android.view.TextureView" + render="skip" /> + <view + class="android.view.SurfaceView" + render="skip" /> + <view + class="android.widget.NumberPicker" + topAttrs="focusable,focusableInTouchMode" + relatedTo="TimePicker,DatePicker" + render="alone" /> + <view + class="android.widget.ZoomButton" + topAttrs="background" + relatedTo="Button,ZoomControls" /> + <view + class="android.widget.ZoomControls" + topAttrs="style,background,gravity" + relatedTo="ZoomButton" + resize="none" /> + <view + class="merge" + topAttrs="orientation,gravity,style" + skip="true" + render="skip" /> + <view + class="android.widget.DialerFilter" + fill="width_in_vertical" + render="skip" /> + <view + class="android.widget.TwoLineListItem" + topAttrs="mode,paddingBottom,paddingTop,minHeight,paddingLeft" + render="skip" /> + <view + class="android.widget.AbsoluteLayout" + topAttrs="background,orientation,paddingBottom,paddingLeft,paddingRight,paddingTop" + name="AbsoluteLayout (Deprecated)" + fill="opposite" + render="skip" /> + </category> + <category + name="Other"> + <!-- This is the catch-all category which contains unknown views if we encounter any --> + </category> + <!-- TODO: Add-ons? --> +</metadata> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/rendering-configs.xml b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/rendering-configs.xml new file mode 100644 index 000000000..96c7fe7d2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/rendering-configs.xml @@ -0,0 +1,382 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Default configuration for various views to be rendered + TODO: Remove views that don't have custom configuration + TODO: Parameterize the custom width (200dip) in the below? +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <AnalogClock + android:layout_width="wrap_content" + android:id="@+id/android_widget_AnalogClock" + android:layout_height="75dip"> + </AnalogClock> + <AutoCompleteTextView + android:layout_height="wrap_content" + android:layout_width="200dip" + android:text="AutoComplete" + android:id="@+id/android_widget_AutoCompleteTextView"> + </AutoCompleteTextView> + <Button + android:text="Button" + android:id="@+id/android_widget_Button" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + </Button> + <Button + android:text="Small" + style="?android:attr/buttonStyleSmall" + android:id="@+id/android_widget_SmallButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + </Button> + <CheckBox + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:text="CheckBox" + android:id="@+id/android_widget_CheckBox" + android:checked="true"> + </CheckBox> + <CheckedTextView + android:text="CheckedTextView" + android:id="@+id/android_widget_CheckedTextView" + android:layout_height="wrap_content" + android:layout_width="wrap_content"> + </CheckedTextView> + <!-- + <Chronometer + android:text="Chronometer" + android:id="@+id/android_widget_Chronometer" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + </Chronometer> + --> + <DigitalClock + android:text="DigitalClock" + android:id="@+id/android_widget_DigitalClock" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + </DigitalClock> + + <EditText + android:id="@+id/PlainText" + android:text="abc" + android:layout_width="200dip" + android:layout_height="wrap_content"> + </EditText> + + <EditText + android:id="@+id/Password" + android:inputType="textPassword" + android:text="••••••••" + android:layout_width="200dip" + android:layout_height="wrap_content"> + </EditText> + + <!-- android:inputType="numberPassword" not used here to allow digits in preview only --> + <EditText + android:id="@+id/PasswordNumeric" + android:text="1•••2•••3" + android:layout_width="200dip" + android:layout_height="wrap_content"> + </EditText> + + <EditText + android:id="@+id/PersonName" + android:inputType="textPersonName" + android:text="Firstname Lastname" + android:layout_width="200dip" + android:layout_height="wrap_content"> + </EditText> + + <EditText + android:id="@+id/Phone" + android:inputType="phone" + android:text="(555) 0100" + android:layout_width="200dip" + android:layout_height="wrap_content"> + </EditText> + + <EditText + android:id="@+id/PostalAddress" + android:inputType="textPostalAddress" + android:text="Address" + android:layout_width="200dip" + android:layout_height="100dip"> + </EditText> + + <EditText + android:id="@+id/MultilineText" + android:inputType="textMultiLine" + android:text="Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor" + android:layout_width="200dip" + android:layout_height="100dip"> + </EditText> + + <EditText + android:id="@+id/Date" + android:inputType="date" + android:text="1/1/2011" + android:layout_width="200dip" + android:layout_height="wrap_content"> + </EditText> + + <EditText + android:id="@+id/Time" + android:inputType="time" + android:text="12:00am" + android:layout_width="200dip" + android:layout_height="wrap_content"> + </EditText> + + <EditText + android:id="@+id/Email" + android:inputType="textEmailAddress" + android:text="user@domain" + android:layout_width="200dip" + android:layout_height="wrap_content"> + </EditText> + + <EditText + android:id="@+id/Number" + android:inputType="number" + android:text="42" + android:layout_width="200dip" + android:layout_height="wrap_content"> + </EditText> + + <EditText + android:id="@+id/NumberSigned" + android:inputType="numberSigned" + android:text="-42" + android:layout_width="200dip" + android:layout_height="wrap_content"> + </EditText> + + <EditText + android:id="@+id/NumberDecimal" + android:inputType="numberDecimal" + android:text="42.0" + android:layout_width="200dip" + android:layout_height="wrap_content"> + </EditText> + + <TextView + android:text="Large" + android:id="@+id/LargeText" + android:textAppearance="?android:attr/textAppearanceLarge" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + </TextView> + + <TextView + android:text="Medium" + android:id="@+id/MediumText" + android:textAppearance="?android:attr/textAppearanceMedium" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + </TextView> + + <TextView + android:text="Small" + android:id="@+id/SmallText" + android:textAppearance="?android:attr/textAppearanceSmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + </TextView> + + <MultiAutoCompleteTextView + android:layout_height="wrap_content" + android:layout_width="200dip" + android:text="MultiAutoComplete" + android:id="@+id/android_widget_MultiAutoCompleteTextView"> + </MultiAutoCompleteTextView> + <ProgressBar + android:id="@+id/android_widget_ProgressBarNormal" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + </ProgressBar> + <ProgressBar + android:id="@+id/android_widget_ProgressBarHorizontal" + android:layout_width="200dip" + android:layout_height="wrap_content" + android:progress="30" + style="?android:attr/progressBarStyleHorizontal"> + </ProgressBar> + <ProgressBar + android:id="@+id/android_widget_ProgressBarLarge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="?android:attr/progressBarStyleLarge"> + </ProgressBar> + <ProgressBar + android:id="@+id/android_widget_ProgressBarSmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="?android:attr/progressBarStyleSmall"> + </ProgressBar> + <QuickContactBadge + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:id="@+id/android_widget_QuickContactBadge"> + </QuickContactBadge> + <RadioButton + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:id="@+id/android_widget_RadioButton" + android:text="RadioButton" + android:checked="true"> + </RadioButton> + <RatingBar + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:id="@+id/android_widget_RatingBar" + android:rating="1"> + </RatingBar> + <SeekBar + android:layout_height="wrap_content" + android:id="@+id/android_widget_SeekBar" + android:layout_width="200dip" + android:progress="30"> + </SeekBar> + <ListView + android:id="@+id/android_widget_ListView" + android:layout_width="200dip" + android:layout_height="60dip" + android:divider="#333333" + android:dividerHeight="1px" + > + </ListView> + <ExpandableListView + android:id="@+id/android_widget_ExpandableListView" + android:layout_width="200dip" + android:layout_height="60dip" + android:divider="#333333" + android:dividerHeight="1px" + > + </ExpandableListView> + <Spinner + android:layout_height="wrap_content" + android:id="@+id/android_widget_Spinner" + android:layout_width="200dip"> + </Spinner> + <TextView + android:text="TextView" + android:id="@+id/android_widget_TextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + </TextView> + <ToggleButton + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:checked="false" + android:id="@+id/android_widget_ToggleButton" + android:text="ToggleButton"> + </ToggleButton> + <ZoomButton + android:id="@+id/android_widget_ZoomButton" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:src="@android:drawable/btn_plus"> + </ZoomButton> + <ZoomControls + android:id="@+id/android_widget_ZoomControls" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + </ZoomControls> + <Switch + android:id="@+id/android_widget_Switch" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + <TimePicker + android:id="@+id/android_widget_TimePicker" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + </TimePicker> + <DatePicker + android:id="@+id/android_widget_DatePicker" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + </DatePicker> + <CalendarView + android:id="@+id/android_widget_CalendarView" + android:layout_width="200dip" + android:layout_height="200dip"> + </CalendarView> + <RadioGroup + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:orientation="horizontal" + android:id="@+id/android_widget_RadioGroup"> + <RadioButton + android:checked="true"> + </RadioButton> + <RadioButton></RadioButton> + <RadioButton></RadioButton> + </RadioGroup> + <TabHost + android:id="@android:id/tabhost" + android:layout_width="200dip" + android:layout_height="100dip"> + <LinearLayout + android:id="@+id/linearLayout1" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <TabWidget + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:id="@android:id/tabs"> + </TabWidget> + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@android:id/tabcontent"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/Tab1"> + </LinearLayout> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/Tab2"> + </LinearLayout> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/Tab3"> + </LinearLayout> + </FrameLayout> + </LinearLayout> + </TabHost> + <TabHost + android:id="@android:id/tabhost" + android:layout_width="70dip" + android:layout_height="100dip"> + <LinearLayout + android:id="@+id/android_widget_TabWidget" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <TabWidget + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:id="@android:id/tabs"> + </TabWidget> + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@android:id/tabcontent"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/Tab1"> + </LinearLayout> + </FrameLayout> + </LinearLayout> + </TabHost> +</LinearLayout> |