1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.eclipse.org/org/documents/epl-v10.php
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.ide.eclipse.adt.internal.editors.layout.gre;
18
19import static com.android.SdkConstants.ANDROID_URI;
20import static com.android.SdkConstants.ATTR_ID;
21import static com.android.SdkConstants.AUTO_URI;
22import static com.android.SdkConstants.CLASS_FRAGMENT;
23import static com.android.SdkConstants.CLASS_V4_FRAGMENT;
24import static com.android.SdkConstants.NEW_ID_PREFIX;
25import static com.android.SdkConstants.URI_PREFIX;
26
27import com.android.annotations.NonNull;
28import com.android.annotations.Nullable;
29import com.android.ide.common.api.IClientRulesEngine;
30import com.android.ide.common.api.INode;
31import com.android.ide.common.api.IValidator;
32import com.android.ide.common.api.IViewMetadata;
33import com.android.ide.common.api.IViewRule;
34import com.android.ide.common.api.Margins;
35import com.android.ide.common.api.Rect;
36import com.android.ide.common.layout.BaseViewRule;
37import com.android.ide.common.resources.ResourceRepository;
38import com.android.ide.eclipse.adt.AdtPlugin;
39import com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction;
40import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
41import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
42import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
43import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser;
44import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
45import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
46import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas;
47import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService;
48import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager;
49import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy;
50import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
51import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
52import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
53import com.android.ide.eclipse.adt.internal.resources.CyclicDependencyValidator;
54import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
55import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
56import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
57import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
58import com.android.ide.eclipse.adt.internal.sdk.Sdk;
59import com.android.ide.eclipse.adt.internal.ui.MarginChooser;
60import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog;
61import com.android.ide.eclipse.adt.internal.ui.ResourceChooser;
62import com.android.ide.eclipse.adt.internal.ui.ResourcePreviewHelper;
63import com.android.resources.ResourceType;
64import com.android.sdklib.IAndroidTarget;
65
66import org.eclipse.core.resources.IProject;
67import org.eclipse.core.runtime.CoreException;
68import org.eclipse.core.runtime.NullProgressMonitor;
69import org.eclipse.jdt.core.Flags;
70import org.eclipse.jdt.core.IJavaProject;
71import org.eclipse.jdt.core.IPackageFragment;
72import org.eclipse.jdt.core.IPackageFragmentRoot;
73import org.eclipse.jdt.core.IType;
74import org.eclipse.jdt.core.ITypeHierarchy;
75import org.eclipse.jdt.core.JavaModelException;
76import org.eclipse.jdt.core.search.IJavaSearchScope;
77import org.eclipse.jdt.core.search.SearchEngine;
78import org.eclipse.jdt.ui.IJavaElementSearchConstants;
79import org.eclipse.jdt.ui.JavaUI;
80import org.eclipse.jdt.ui.actions.OpenNewClassWizardAction;
81import org.eclipse.jdt.ui.dialogs.ITypeInfoFilterExtension;
82import org.eclipse.jdt.ui.dialogs.ITypeInfoRequestor;
83import org.eclipse.jdt.ui.dialogs.TypeSelectionExtension;
84import org.eclipse.jdt.ui.wizards.NewClassWizardPage;
85import org.eclipse.jface.dialogs.IDialogConstants;
86import org.eclipse.jface.dialogs.IInputValidator;
87import org.eclipse.jface.dialogs.InputDialog;
88import org.eclipse.jface.dialogs.MessageDialog;
89import org.eclipse.jface.dialogs.ProgressMonitorDialog;
90import org.eclipse.jface.window.Window;
91import org.eclipse.swt.SWT;
92import org.eclipse.swt.events.SelectionAdapter;
93import org.eclipse.swt.events.SelectionEvent;
94import org.eclipse.swt.layout.GridLayout;
95import org.eclipse.swt.widgets.Button;
96import org.eclipse.swt.widgets.Composite;
97import org.eclipse.swt.widgets.Control;
98import org.eclipse.swt.widgets.Display;
99import org.eclipse.swt.widgets.Shell;
100import org.eclipse.ui.dialogs.SelectionDialog;
101import org.w3c.dom.Document;
102import org.w3c.dom.Element;
103import org.w3c.dom.Node;
104import org.w3c.dom.NodeList;
105
106import java.util.Collection;
107import java.util.Collections;
108import java.util.HashSet;
109import java.util.List;
110import java.util.Map;
111import java.util.Set;
112import java.util.concurrent.atomic.AtomicReference;
113
114/**
115 * Implementation of {@link IClientRulesEngine}. This provides {@link IViewRule} clients
116 * with a few methods they can use to access functionality from this {@link RulesEngine}.
117 */
118class ClientRulesEngine implements IClientRulesEngine {
119    private final RulesEngine mRulesEngine;
120    private final String mFqcn;
121
122    public ClientRulesEngine(RulesEngine rulesEngine, String fqcn) {
123        mRulesEngine = rulesEngine;
124        mFqcn = fqcn;
125    }
126
127    @Override
128    public @NonNull String getFqcn() {
129        return mFqcn;
130    }
131
132    @Override
133    public void debugPrintf(@NonNull String msg, Object... params) {
134        AdtPlugin.printToConsole(
135                mFqcn == null ? "<unknown>" : mFqcn,
136                String.format(msg, params)
137                );
138    }
139
140    @Override
141    public IViewRule loadRule(@NonNull String fqcn) {
142        return mRulesEngine.loadRule(fqcn, fqcn);
143    }
144
145    @Override
146    public void displayAlert(@NonNull String message) {
147        MessageDialog.openInformation(
148                AdtPlugin.getDisplay().getActiveShell(),
149                mFqcn,  // title
150                message);
151    }
152
153    @Override
154    public String displayInput(@NonNull String message, @Nullable String value,
155            final @Nullable IValidator filter) {
156        IInputValidator validator = null;
157        if (filter != null) {
158            validator = new IInputValidator() {
159                @Override
160                public String isValid(String newText) {
161                    // IValidator has the same interface as SWT's IInputValidator
162                    try {
163                        return filter.validate(newText);
164                    } catch (Exception e) {
165                        AdtPlugin.log(e, "Custom validator failed: %s", e.toString());
166                        return ""; //$NON-NLS-1$
167                    }
168                }
169            };
170        }
171
172        InputDialog d = new InputDialog(
173                    AdtPlugin.getDisplay().getActiveShell(),
174                    mFqcn,  // title
175                    message,
176                    value == null ? "" : value, //$NON-NLS-1$
177                    validator);
178        if (d.open() == Window.OK) {
179            return d.getValue();
180        }
181        return null;
182    }
183
184    @Override
185    @Nullable
186    public Object getViewObject(@NonNull INode node) {
187        ViewHierarchy views = mRulesEngine.getEditor().getCanvasControl().getViewHierarchy();
188        CanvasViewInfo vi = views.findViewInfoFor(node);
189        if (vi != null) {
190            return vi.getViewObject();
191        }
192
193        return null;
194    }
195
196    @Override
197    public @NonNull IViewMetadata getMetadata(final @NonNull String fqcn) {
198        return new IViewMetadata() {
199            @Override
200            public @NonNull String getDisplayName() {
201                // This also works when there is no "."
202                return fqcn.substring(fqcn.lastIndexOf('.') + 1);
203            }
204
205            @Override
206            public @NonNull FillPreference getFillPreference() {
207                return ViewMetadataRepository.get().getFillPreference(fqcn);
208            }
209
210            @Override
211            public @NonNull Margins getInsets() {
212                return mRulesEngine.getEditor().getCanvasControl().getInsets(fqcn);
213            }
214
215            @Override
216            public @NonNull List<String> getTopAttributes() {
217                return ViewMetadataRepository.get().getTopAttributes(fqcn);
218            }
219        };
220    }
221
222    @Override
223    public int getMinApiLevel() {
224        Sdk currentSdk = Sdk.getCurrent();
225        if (currentSdk != null) {
226            IAndroidTarget target = currentSdk.getTarget(mRulesEngine.getEditor().getProject());
227            if (target != null) {
228                return target.getVersion().getApiLevel();
229            }
230        }
231
232        return -1;
233    }
234
235    @Override
236    public IValidator getResourceValidator(
237            @NonNull final String resourceTypeName, final boolean uniqueInProject,
238            final boolean uniqueInLayout, final boolean exists, final String... allowed) {
239        return new IValidator() {
240            private ResourceNameValidator mValidator;
241
242            @Override
243            public String validate(@NonNull String text) {
244                if (mValidator == null) {
245                    ResourceType type = ResourceType.getEnum(resourceTypeName);
246                    if (uniqueInLayout) {
247                        assert !uniqueInProject;
248                        assert !exists;
249                        Set<String> existing = new HashSet<String>();
250                        Document doc = mRulesEngine.getEditor().getModel().getXmlDocument();
251                        if (doc != null) {
252                            addIds(doc, existing);
253                        }
254                        for (String s : allowed) {
255                            existing.remove(s);
256                        }
257                        mValidator = ResourceNameValidator.create(false, existing, type);
258                    } else {
259                        assert allowed.length == 0;
260                        IProject project = mRulesEngine.getEditor().getProject();
261                        mValidator = ResourceNameValidator.create(false, project, type);
262                        if (uniqueInProject) {
263                            mValidator.unique();
264                        }
265                    }
266                    if (exists) {
267                        mValidator.exist();
268                    }
269                }
270
271                return mValidator.isValid(text);
272            }
273        };
274    }
275
276    /** Find declared ids under the given DOM node */
277    private static void addIds(Node node, Set<String> ids) {
278        if (node.getNodeType() == Node.ELEMENT_NODE) {
279            Element element = (Element) node;
280            String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
281            if (id != null && id.startsWith(NEW_ID_PREFIX)) {
282                ids.add(BaseViewRule.stripIdPrefix(id));
283            }
284        }
285
286        NodeList children = node.getChildNodes();
287        for (int i = 0, n = children.getLength(); i < n; i++) {
288            Node child = children.item(i);
289            addIds(child, ids);
290        }
291    }
292
293    @Override
294    public String displayReferenceInput(@Nullable String currentValue) {
295        GraphicalEditorPart graphicalEditor = mRulesEngine.getEditor();
296        LayoutEditorDelegate delegate = graphicalEditor.getEditorDelegate();
297        IProject project = delegate.getEditor().getProject();
298        if (project != null) {
299            // get the resource repository for this project and the system resources.
300            ResourceRepository projectRepository =
301                ResourceManager.getInstance().getProjectResources(project);
302            Shell shell = AdtPlugin.getDisplay().getActiveShell();
303            if (shell == null) {
304                return null;
305            }
306            ReferenceChooserDialog dlg = new ReferenceChooserDialog(
307                    project,
308                    projectRepository,
309                    shell);
310            dlg.setPreviewHelper(new ResourcePreviewHelper(dlg, graphicalEditor));
311
312            dlg.setCurrentResource(currentValue);
313
314            if (dlg.open() == Window.OK) {
315                return dlg.getCurrentResource();
316            }
317        }
318
319        return null;
320    }
321
322    @Override
323    public String displayResourceInput(@NonNull String resourceTypeName,
324            @Nullable String currentValue) {
325        return displayResourceInput(resourceTypeName, currentValue, null);
326    }
327
328    private String displayResourceInput(String resourceTypeName, String currentValue,
329            IInputValidator validator) {
330        ResourceType type = ResourceType.getEnum(resourceTypeName);
331        GraphicalEditorPart graphicalEditor = mRulesEngine.getEditor();
332        return ResourceChooser.chooseResource(graphicalEditor, type, currentValue, validator);
333    }
334
335    @Override
336    public String[] displayMarginInput(@Nullable String all, @Nullable String left,
337            @Nullable String right, @Nullable String top, @Nullable String bottom) {
338        GraphicalEditorPart editor = mRulesEngine.getEditor();
339        IProject project = editor.getProject();
340        if (project != null) {
341            Shell shell = AdtPlugin.getDisplay().getActiveShell();
342            if (shell == null) {
343                return null;
344            }
345            AndroidTargetData data = editor.getEditorDelegate().getEditor().getTargetData();
346            MarginChooser dialog = new MarginChooser(shell, editor, data, all, left, right,
347                    top, bottom);
348            if (dialog.open() == Window.OK) {
349                return dialog.getMargins();
350            }
351        }
352
353        return null;
354    }
355
356    @Override
357    public String displayIncludeSourceInput() {
358        AndroidXmlEditor editor = mRulesEngine.getEditor().getEditorDelegate().getEditor();
359        IInputValidator validator = CyclicDependencyValidator.create(editor.getInputFile());
360        return displayResourceInput(ResourceType.LAYOUT.getName(), null, validator);
361    }
362
363    @Override
364    public void select(final @NonNull Collection<INode> nodes) {
365        LayoutCanvas layoutCanvas = mRulesEngine.getEditor().getCanvasControl();
366        final SelectionManager selectionManager = layoutCanvas.getSelectionManager();
367        selectionManager.select(nodes);
368        // ALSO run an async select since immediately after nodes are created they
369        // may not be selectable. We can't ONLY run an async exec since
370        // code may depend on operating on the selection.
371        layoutCanvas.getDisplay().asyncExec(new Runnable() {
372            @Override
373            public void run() {
374                selectionManager.select(nodes);
375            }
376        });
377    }
378
379    @Override
380    public String displayFragmentSourceInput() {
381        try {
382            // Compute a search scope: We need to merge all the subclasses
383            // android.app.Fragment and android.support.v4.app.Fragment
384            IJavaSearchScope scope = SearchEngine.createWorkspaceScope();
385            IProject project = mRulesEngine.getProject();
386            final IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
387            if (javaProject != null) {
388                IType oldFragmentType = javaProject.findType(CLASS_V4_FRAGMENT);
389
390                // First check to make sure fragments are available, and if not,
391                // warn the user.
392                IAndroidTarget target = Sdk.getCurrent().getTarget(project);
393                // No, this should be using the min SDK instead!
394                if (target.getVersion().getApiLevel() < 11 && oldFragmentType == null) {
395                    // Compatibility library must be present
396                    MessageDialog dialog =
397                        new MessageDialog(
398                                Display.getCurrent().getActiveShell(),
399                          "Fragment Warning",
400                          null,
401                          "Fragments require API level 11 or higher, or a compatibility "
402                          + "library for older versions.\n\n"
403                          + " Do you want to install the compatibility library?",
404                          MessageDialog.QUESTION,
405                          new String[] { "Install", "Cancel" },
406                          1 /* default button: Cancel */);
407                      int answer = dialog.open();
408                      if (answer == 0) {
409                          if (!AddSupportJarAction.install(project)) {
410                              return null;
411                          }
412                      } else {
413                          return null;
414                      }
415                }
416
417                // Look up sub-types of each (new fragment class and compatibility fragment
418                // class, if any) and merge the two arrays - then create a scope from these
419                // elements.
420                IType[] fragmentTypes = new IType[0];
421                IType[] oldFragmentTypes = new IType[0];
422                if (oldFragmentType != null) {
423                    ITypeHierarchy hierarchy =
424                        oldFragmentType.newTypeHierarchy(new NullProgressMonitor());
425                    oldFragmentTypes = hierarchy.getAllSubtypes(oldFragmentType);
426                }
427                IType fragmentType = javaProject.findType(CLASS_FRAGMENT);
428                if (fragmentType != null) {
429                    ITypeHierarchy hierarchy =
430                        fragmentType.newTypeHierarchy(new NullProgressMonitor());
431                    fragmentTypes = hierarchy.getAllSubtypes(fragmentType);
432                }
433                IType[] subTypes = new IType[fragmentTypes.length + oldFragmentTypes.length];
434                System.arraycopy(fragmentTypes, 0, subTypes, 0, fragmentTypes.length);
435                System.arraycopy(oldFragmentTypes, 0, subTypes, fragmentTypes.length,
436                        oldFragmentTypes.length);
437                scope = SearchEngine.createJavaSearchScope(subTypes, IJavaSearchScope.SOURCES);
438            }
439
440            Shell parent = AdtPlugin.getDisplay().getActiveShell();
441            final AtomicReference<String> returnValue =
442                new AtomicReference<String>();
443            final AtomicReference<SelectionDialog> dialogHolder =
444                new AtomicReference<SelectionDialog>();
445            final SelectionDialog dialog = JavaUI.createTypeDialog(
446                    parent,
447                    new ProgressMonitorDialog(parent),
448                    scope,
449                    IJavaElementSearchConstants.CONSIDER_CLASSES, false,
450                    // Use ? as a default filter to fill dialog with matches
451                    "?", //$NON-NLS-1$
452                    new TypeSelectionExtension() {
453                        @Override
454                        public Control createContentArea(Composite parentComposite) {
455                            Composite composite = new Composite(parentComposite, SWT.NONE);
456                            composite.setLayout(new GridLayout(1, false));
457                            Button button = new Button(composite, SWT.PUSH);
458                            button.setText("Create New...");
459                            button.addSelectionListener(new SelectionAdapter() {
460                                @Override
461                                public void widgetSelected(SelectionEvent e) {
462                                    String fqcn = createNewFragmentClass(javaProject);
463                                    if (fqcn != null) {
464                                        returnValue.set(fqcn);
465                                        dialogHolder.get().close();
466                                    }
467                                }
468                            });
469                            return composite;
470                        }
471
472                        @Override
473                        public ITypeInfoFilterExtension getFilterExtension() {
474                            return new ITypeInfoFilterExtension() {
475                                @Override
476                                public boolean select(ITypeInfoRequestor typeInfoRequestor) {
477                                    int modifiers = typeInfoRequestor.getModifiers();
478                                    if (!Flags.isPublic(modifiers)
479                                            || Flags.isInterface(modifiers)
480                                            || Flags.isEnum(modifiers)) {
481                                        return false;
482                                    }
483                                    return true;
484                                }
485                            };
486                        }
487                    });
488            dialogHolder.set(dialog);
489
490            dialog.setTitle("Choose Fragment Class");
491            dialog.setMessage("Select a Fragment class (? = any character, * = any string):");
492            if (dialog.open() == IDialogConstants.CANCEL_ID) {
493                return null;
494            }
495            if (returnValue.get() != null) {
496                return returnValue.get();
497            }
498
499            Object[] types = dialog.getResult();
500            if (types != null && types.length > 0) {
501                return ((IType) types[0]).getFullyQualifiedName();
502            }
503        } catch (JavaModelException e) {
504            AdtPlugin.log(e, null);
505        } catch (CoreException e) {
506            AdtPlugin.log(e, null);
507        }
508        return null;
509    }
510
511    @Override
512    public void redraw() {
513        mRulesEngine.getEditor().getCanvasControl().redraw();
514    }
515
516    @Override
517    public void layout() {
518        mRulesEngine.getEditor().recomputeLayout();
519    }
520
521    @Override
522    public Map<INode, Rect> measureChildren(@NonNull INode parent,
523            @Nullable IClientRulesEngine.AttributeFilter filter) {
524        RenderService renderService = RenderService.create(mRulesEngine.getEditor());
525        Map<INode, Rect> map = renderService.measureChildren(parent, filter);
526        if (map == null) {
527            map = Collections.emptyMap();
528        }
529        return map;
530    }
531
532    @Override
533    public int pxToDp(int px) {
534        ConfigurationChooser chooser = mRulesEngine.getEditor().getConfigurationChooser();
535        float dpi = chooser.getConfiguration().getDensity().getDpiValue();
536        return (int) (px * 160 / dpi);
537    }
538
539    @Override
540    public int dpToPx(int dp) {
541        ConfigurationChooser chooser = mRulesEngine.getEditor().getConfigurationChooser();
542        float dpi = chooser.getConfiguration().getDensity().getDpiValue();
543        return (int) (dp * dpi / 160);
544    }
545
546    @Override
547    public int screenToLayout(int pixels) {
548        return (int) (pixels / mRulesEngine.getEditor().getCanvasControl().getScale());
549    }
550
551    String createNewFragmentClass(IJavaProject javaProject) {
552        NewClassWizardPage page = new NewClassWizardPage();
553
554        IProject project = mRulesEngine.getProject();
555        IAndroidTarget target = Sdk.getCurrent().getTarget(project);
556        String superClass;
557        if (target.getVersion().getApiLevel() < 11) {
558            superClass = CLASS_V4_FRAGMENT;
559        } else {
560            superClass = CLASS_FRAGMENT;
561        }
562        page.setSuperClass(superClass, true /* canBeModified */);
563        IPackageFragmentRoot root = ManifestInfo.getSourcePackageRoot(javaProject);
564        if (root != null) {
565            page.setPackageFragmentRoot(root, true /* canBeModified */);
566        }
567        ManifestInfo manifestInfo = ManifestInfo.get(project);
568        IPackageFragment pkg = manifestInfo.getPackageFragment();
569        if (pkg != null) {
570            page.setPackageFragment(pkg, true /* canBeModified */);
571        }
572        OpenNewClassWizardAction action = new OpenNewClassWizardAction();
573        action.setConfiguredWizardPage(page);
574        action.run();
575        IType createdType = page.getCreatedType();
576        if (createdType != null) {
577            return createdType.getFullyQualifiedName();
578        } else {
579            return null;
580        }
581    }
582
583    @Override
584    public @NonNull String getUniqueId(@NonNull String fqcn) {
585        UiDocumentNode root = mRulesEngine.getEditor().getModel();
586        String prefix = fqcn.substring(fqcn.lastIndexOf('.') + 1);
587        prefix = Character.toLowerCase(prefix.charAt(0)) + prefix.substring(1);
588        return DescriptorsUtils.getFreeWidgetId(root, prefix);
589    }
590
591    @Override
592    public @NonNull String getAppNameSpace() {
593        IProject project = mRulesEngine.getEditor().getProject();
594
595        ProjectState projectState = Sdk.getProjectState(project);
596        if (projectState != null && projectState.isLibrary()) {
597            return AUTO_URI;
598        }
599
600        ManifestInfo info = ManifestInfo.get(project);
601        return URI_PREFIX + info.getPackage();
602    }
603}
604