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