/* * 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.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)) { 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 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()); } String createNewFragmentClass(IJavaProject javaProject) { NewClassWizardPage page = new NewClassWizardPage(); IProject project = mRulesEngine.getProject(); IAndroidTarget target = Sdk.getCurrent().getTarget(project); String superClass; if (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; } } @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(); } }