aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java762
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactory.java86
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java517
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/PaletteMetadataDescriptor.java120
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RuleLoader.java192
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java876
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java856
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml452
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/rendering-configs.xml382
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 &amp; 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 &amp; 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>